Mini-Challenge Beschreibung

Daten und Libraries

library(tidyverse)
Registered S3 methods overwritten by 'dbplyr':
  method         from
  print.tbl_lazy     
  print.tbl_sql      
── Attaching packages ──────────────────────────────────────────────────────────────────────────────────────────────────────── tidyverse 1.3.2 ──✔ ggplot2 3.4.0      ✔ purrr   0.3.5 
✔ tibble  3.1.8      ✔ dplyr   1.0.10
✔ tidyr   1.2.1      ✔ stringr 1.5.0 
✔ readr   2.1.3      ✔ forcats 0.5.2 ── Conflicts ─────────────────────────────────────────────────────────────────────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
library(recommenderlab)
Loading required package: Matrix

Attaching package: ‘Matrix’

The following objects are masked from ‘package:tidyr’:

    expand, pack, unpack

Loading required package: arules

Attaching package: ‘arules’

The following object is masked from ‘package:dplyr’:

    recode

The following objects are masked from ‘package:base’:

    abbreviate, write

Loading required package: proxy

Attaching package: ‘proxy’

The following object is masked from ‘package:Matrix’:

    as.matrix

The following objects are masked from ‘package:stats’:

    as.dist, dist

The following object is masked from ‘package:base’:

    as.matrix

Loading required package: registry
library(gridExtra)

Attaching package: ‘gridExtra’

The following object is masked from ‘package:dplyr’:

    combine

Daten einlesen

data(MovieLense)

Beim Einlesen des Datensatzes werden drei realRatingMatrix eingelesen: MovieLense, MovieLenseMeta und MovieLenseUser. Wir untersuchen nun zuerst MovieLense.

Library Methoden zum Datensatz MovieLense

methods(class = class(MovieLense))
 [1] [                      [<-                    binarize               calcPredictionAccuracy coerce                 colCounts             
 [7] colMeans               colSds                 colSums                denormalize            dim                    dimnames              
[13] dimnames<-             dissimilarity          evaluationScheme       getData.frame          getList                getNormalize          
[19] getRatingMatrix        getRatings             getTopNLists           hasRating              image                  normalize             
[25] nratings               Recommender            removeKnownRatings     rowCounts              rowMeans               rowSds                
[31] rowSums                sample                 show                   similarity            
see '?methods' for accessing help and source code

Diese Übersicht zeigt uns, welche Methoden mit der realRatingMatrix in Kombination mit Recommenderlab möglich sind.

MovieLense Dataframe erstellen

MovieLenseEDA <- as(MovieLense, "data.frame")

Um den EDA-Teil lösen zu können, haben wir die realRatingMatrix in einen data.frame umgewandelt.

Kopfzeile und Fusszeile vom Datensatz ausgeben

head(MovieLenseEDA)

tail(MovieLenseEDA)

Um eine Idee der Daten zu erhalten, haben wir den Head und Tail des Dataframes ausgegeben. Es wird ersichtlich, dass für jede Zeile ein User, Item (Film) und das Rating erfasst sind.

Infos zum Datensatz

summary(MovieLenseEDA)
     user               item               rating    
 Length:99392       Length:99392       Min.   :1.00  
 Class :character   Class :character   1st Qu.:3.00  
 Mode  :character   Mode  :character   Median :4.00  
                                       Mean   :3.53  
                                       3rd Qu.:4.00  
                                       Max.   :5.00  

Mit der Summary Funktion haben wir uns einen Überblick über die Zahlen im Datensatz erschaffen. Es sind jeweils 99’392 User und Items erfasst. Die Ratings reichen vom Bereich 1 bis 5 und der Mittelwert beträgt 3.53 (Median 4.0)

1 Explorative Datenanalyse

Aufgabe 1: Untersuche den vollständigen MovieLense Datensatz (d.h. vor Datenreduktion!) und beantworte folgende Fragen:

1.1 Welches sind die am häufigsten geschauten Genres/Filme?

1.1.1 Welches sind die am häufigsten geschauten Filme?

MovieLenseEDA %>% 
  group_by(item) %>% 
  summarise(Anzahl = n()) %>%
  arrange(desc(Anzahl)) %>% 
  head(n=10)

Diese Tabelle zeigt uns die Top-10 der am meisten geschauten, resp. gerateten Filme. Es wird ersichtlich, dass Star Wars (1977, vermutlich “Krieg der Sterne”) 583 mal geschaut und geratet wurde.

1.1.2 Welches sind die am häufigsten geschauten Genre?

# Full Join mit df_movies_rating und MovieLenseMeta
MovieLenseEDA_Joined <- full_join(MovieLenseEDA, MovieLenseMeta, 
          by = c("item" = "title")) %>% 
  select(-c("user", "item", "rating", "year", "url")) 

# Aufsummieren der Genre Spalten
(colSums(MovieLenseEDA_Joined)) %>% sort(decreasing = TRUE)
      Drama      Comedy      Action    Thriller     Romance   Adventure      Sci-Fi         War       Crime  Children's      Horror     Mystery 
      39446       29778       25510       21808       19203       13688       12694        9398        8027        7143        5280        5237 
    Musical   Animation     Western   Film-Noir     Fantasy Documentary     unknown 
       4954        3605        1854        1733        1352         758          10 

Wir erkennen, dass das am häufigsten geschauten Genre “Drama” mit 39’446 Ratings ist. Auf dem zweiten Platz befindet sich “Comdey” und auf dem dritten “Action”. Am wenigsten häufig wurden “Documentary” und “unknown” geschaut.

1.2 Wie verteilen sich die Kundenratings gesamthaft und nach Genres?

# DataFrame join
MovieLenseEDA_Joined <- full_join(MovieLenseEDA, MovieLenseMeta, 
          by = c("item" = "title"))

Für diese Frage haben wir einen neuen Datensatz “MovieLenseEDA_Joined” erstellt. Er ergibt sich aus MovieLenseEDA und MovieLenseMeta. Folgend nun die Beantwortund der Frage.

1.2.1 Verteilung der Kundenratings Gesamthaft

MovieLenseEDA_Joined$rating <- as.factor(MovieLenseEDA_Joined$rating)

# Dataframe Uebersicht
MovieLenseEDA_Joined %>% group_by(rating) %>%
  summarize(Anzahl = n())

# Visuelle Darstellung mittels Barplot
MovieLenseEDA_Joined %>% group_by(rating) %>%
  summarize(Anzahl = n()) %>% 
  ggplot(aes(x = rating, y = Anzahl)) + 
  geom_bar(stat = "identity", 
           fill = "lightblue", 
           color = "black") + 
  labs(x = "Ratings", 
       y = "Anzahl", 
       title = "Verteilung der Kundenratings Gesamthaft",
       subtitle = paste("Gesamte Anzahl Kundenratings:", dim(MovieLenseEDA_Joined)[1]))

Wie wir in im Dataframe sowie im Barplot erkennen, werden am häufigsten die Ratings 3 und 4 vergeben. Rating von 1 und 2 kommen deutlich weniger vor, als möglicher Grund könnte sein, dass Filme die schlecht sind gar nicht bewertet wurden, da man sich nicht mehr weiter mit schlechten Filmen befassen möchte. Aus eigenen Erfahrungen können wir sagen, dass man eher mehr bereit ist einen Film zu bewerten, wenn diese auch wirklich gut ist. Das Rating 5 kommt am dritthäufigsten vor.

1.2.2 Verteilung der Kundenratings nach Genre

MovieLenseEDA_Joined$rating <- as.integer(MovieLenseEDA_Joined$rating)

MovieLenseEDA_Joined %>% 
  select(-c("item", "user", "year", "url")) %>% 
  pivot_longer(cols=c("unknown", "Action", "Adventure", "Animation", "Children's",
                      "Comedy", "Crime", "Documentary", "Drama", "Fantasy",
                      "Film-Noir", "Horror", "Musical", "Mystery", "Horror",
                      "Musical", "Mystery", "Romance", "Sci-Fi", "Thriller",
                      "War", "Western"),
               names_to = "Genre", values_to = "is_genre") %>%
  filter(is_genre == 1) %>% 
  ggplot(aes(x = rating)) +
  geom_bar(fill = "lightblue", color = "black") +
  labs(x = "Ratings", 
       y = "Anzahl", 
       title = "Verteilung der Kundenratings nach Genre",
       subtitle = paste("Gesamte Anzahl Kundenratings:", dim(MovieLenseEDA_Joined)[1])) + 
  facet_wrap(~Genre)

In der Visualisierung der Verteilung der Kundenratings pro Genre erkennen wir analog, wie bei der Verteilung der gesamthaften Kundenratings, dass die Rating 3 und 4 am meisten vergeben werden. Dieses Muster ist bei fast allen Genres erkennbar, einfach mit unterschiedlicher Intensität (Anzahl Ratings).

1.3 Wie verteilen sich die mittleren Kundenratings pro Film?

# Dataframe
MovieLenseEDA %>% 
  group_by(item) %>% 
  summarize(mean_rating_per_film = mean(rating),
            n_rating_per_film = n()) %>% 
  arrange(n_rating_per_film)

# Visualisierung
MovieLenseEDA %>% 
  group_by(item) %>% 
  summarize(mean_rating_per_film = mean(rating)) %>% 
  ggplot(aes(x = mean_rating_per_film)) + 
  geom_histogram(color = "black", fill = "lightblue", binwidth = 0.1) +
    labs(x = "Ratings", 
       y = "Anzahl", 
       title = "Verteilung der Mittleren Kundenratings pro Film",
       subtitle = paste("Gesamte Anzahl Kundenratings:", dim(MovieLenseEDA)[1]))

Wir erkennen im Plot die Verteilung durchschnittliche Rating pro Film. Auch hier ist erkennbar, dass die meisten Ratings zwischen 3 und 4 liegen. Einen Ausreisser gibt es beim Rating 1. Bei den natürlichen/ganzzähligen Zahlen erkennen wir ein überraschendes Muster: Die Anzahl erscheint jeweils höher als bei den umliegenden Ratings mit Kommastellen. Dies liegt daran, dass es Filme gibt die nur eine oder wenige Bewertungen bekommen haben (siehe ausgegebenes Datafarme).

1.4 Wie stark streuen die Ratings von individuellen Kunden?

MovieLenseEDA %>% filter(user == c(1:9)) %>% 
  ggplot(aes(x = user, y = rating)) +
  geom_violin(color = "black", fill = "lightblue") +
  labs(x = "User", 
       y = "Ratings", 
       title = "Streueung der Ratings von individuellen Kunden",
       subtitle = "MovieLenseData, Kunden 1-9")

Im Violinenplot stellen wir die ersten 9 User und deren Rating Verteilungen dar. Wir erkennen im Plot, dass User 2 und 8 Filme sehr ähnliche bewerten. Beide bewerten Filme öfters mit einer 4 und eher weniger eine 3 und 5, aber nie 2 und 1. User 5 und 9 bewerten Filme hingegen im ganzen Bereich.

1.5 Welchen Einfluss hat die Normierung der Ratings pro Kunde auf deren Verteilung?

MovieLensenormalized <- normalize(MovieLense)
MovieLenseEDA_Normalized <- (as(MovieLensenormalized, "data.frame"))

MovieLenseEDA_Normalized %>% filter(user == c(1:9)) %>% 
  ggplot(aes(x = user, y = rating)) +
  geom_violin(color = "black", fill = "lightblue") +
  labs(x = "User", 
       y = "Normalisierte Ratings", 
       title = "Normalisierte Streueung der Ratings von individuellen Kunden",
       subtitle = "MovieLenseData, Kunden 1 - 9")

Für die Normierung der Daten haben wir die Funktion von Recommenderlab verwendet. Der Mittelwert der Ratings pro User beträgt nun Null. Im Plot verschiebt sich nun nicht nur die y-Achse, sondern auch die Bandbreite. user 5 und 9, die auf den Rohdaten 1-5 bewertet haben, haben nun unterschiedliche Bandbreiten. Dies liegt daran, dass der Mittelwert der beiden User unterschiedlich ist.

1.6 Welche strukturellen Charakteristika (z.B. Sparsity) und Auffälligkeiten zeigt die User-Item Matrix?

image(x = MovieLense, 
      xlab = "Items", 
      ylab = "Users", 
      main = "Sparisty 943 x 1664 User-Item Matrix 943 x 1664") 


image(MovieLense[1:50,1:50],
      xlab = "Items",
      ylab = "Users", 
      main = "Sparisty 50 x 50 User-Item Matrix")


# nratings(MovieLense) zaehlt die Anzahl vorhandenen Kombinationen von User und Items
(nratings(MovieLense) / (dim(MovieLense)[1] * dim(MovieLense)[2]) * 100)
[1] 6.334122

Für die Darstellung der Sparsity haben wir die image Funktion von Recommenderlab verwendet. Jede Zeile von MovieLense entspricht einem Benutzer und jede Spalte einem Film und für jede geschaute Kombination wird ein Pixel in Graustufen, je nach Rating, markiert. Im ersten Plot wird ersichtlich, dass die ersten User weniger Filme bewertet haben, denn oben rechts sind keine Punkte mehr ersichtlich. Auch ist auffällig, dass die ersten etwa 500 häufiger geschaut wurden, denn bis zu diesem Bereich sind am meisten Pixel eingefärbt. Um die Darstellung genau verstehen zu können, haben wir im zweiten Plot nur die ersten 50 User und Items dargestellt. Dort ist die hohe Sparsity gut erkennbar. Gesamthaft gibt es 943 x 1664 = 1’569’152 Kombinationen zwischen User und Film. Allerdings hat nicht jeder Nutzer jeden Film gesehen, aus diesem Grund ist es wichtig die sparsity der Matrix zu betrachten. In MovieLense Matrix fehlen ca. 94% der Kombinationen. Nur für 6.3% der möglichen Kombinationen sind Ratings vorhanden.

2 Datenreduktion

Aufgabe 2: Reduziere den MovieLense Datensatz auf rund 400 Kunden und 700 Filme, indem du Filme und Kunden mit sehr wenigen Ratings entfernst.

2.1 Vorbereitung

2.1.1 DataFrame neu einlesen

MovieLenseToCut <- as(MovieLense, "data.frame")
MovieLenseToCut

2.1.2 Auswahl der 400 Kunden

select_user_400 <- function(movie_df, start, end) {
  selected_user <- movie_df %>% 
    group_by(user) %>% 
    summarize(Anzahl = n()) %>% 
    arrange(desc(Anzahl)) %>% 
    slice(start:end)
  selected_user
}

MovieLense400User_1 <- select_user_400(MovieLenseToCut, 0, 400)
MovieLense400User_1

MovieLense400User_2 <- select_user_400(MovieLenseToCut, 200, 599)
MovieLense400User_2

Bei der Auswahl der 400 User haben wir direkt auch zwei Dataframes erstellt, da wir die MC zu zweit bearbeiten. Für Person 1 haben wir die 400 User mit den meisten Ratings ausgewählt und für Person 2 User 200 bis 600. Wir haben dieses Vorgehen gewählt um sicherzustellen, dass nur eine Teil der User in beiden Dataframes enthalten ist. Alternativ hätten wir von den Top 500 User zufällig 80% für Person 1 und 2 verwendet, dann hätte die Überlappung aber sehr hoch sein können, so ist es nur die Hälft.

2.1.3 Auswahl der 700 Movies

select_item_700 <- function(movie_df, start, end) {
  selected_item <- MovieLenseToCut %>% 
  group_by(item) %>% 
  summarise(Anzahl = n()) %>% 
  arrange(desc(Anzahl)) %>% 
  slice(start:end)
}

MovieLense700Items_1 <- select_item_700(MovieLenseToCut, 0, 700)
MovieLense700Items_1

MovieLense700Items_2 <- select_item_700(MovieLenseToCut, 150, 849)
MovieLense700Items_2
NA

Das gleiche Vorgehen haben wir bei den Filmen gewählt.

2.1.4 DataFrame schneiden

df_cutter <- function(movie_df, selected_user, selected_items) {
  movie_df_cut <- movie_df %>%
      filter(user %in% c(selected_user$user))
  movie_df_cut <- movie_df_cut %>% 
      filter(item %in% c(selected_items$item))
  movie_df_cut
}

MovieLenseCut_1 <- df_cutter(MovieLenseToCut, MovieLense400User_1, MovieLense700Items_1)
MovieLenseCut_1

MovieLenseCut_2 <- df_cutter(MovieLenseToCut, MovieLense400User_2, MovieLense700Items_2)
MovieLenseCut_2

Untersuche und dokumentiere die Eigenschaften des reduzierten Datensatzes und beschreibe den Effekt der Datenreduktion, d.h.

2.2 Anzahl Filme und Kunden sowie Sparsity vor und nach Datenreduktion,

2.2.1 Vor der Datenreduktion

image(MovieLense, 
      xlab = "Items", 
      ylab = "Users", 
      main = "Vor Datenreduktion, User-Item Matrix 943 x 1664") 


sparsity_text <- function(realrating_matrix) {
  print(paste("Anzahl vorhandene User-Item Rating in", nratings(realrating_matrix) / (dim(realrating_matrix)[1] * dim(realrating_matrix)[2]) * 100, "%"))
  print(paste("Sparsity der Matrix", 100 - (nratings(realrating_matrix) / (dim(realrating_matrix)[1] * dim(realrating_matrix)[2]) * 100), "%"))
}

sparsity_text(MovieLense)
[1] "Anzahl vorhandene User-Item Rating in 6.33412186964679 %"
[1] "Sparsity der Matrix 93.6658781303532 %"

Zur Repetition stellen wir nochmals die Sparsity als Bild dar und berechnen den Wert.

2.2.2 Nach der 1. Datenreduktion

MovieLenseCompact_1 <- as(MovieLenseCut_1, "realRatingMatrix")
image(MovieLenseCompact_1,
      xlab = "Items", 
      ylab = "Users", 
      main = "Nach Datenreduktion 1, User-Item Matrix 400 x 700")


sparsity_text(MovieLenseCompact_1)
[1] "Anzahl vorhandene User-Item Rating in 24.0810714285714 %"
[1] "Sparsity der Matrix 75.9189285714286 %"

Für den ersten Datensatz wird ersichtlich, dass die Ratings gegenüber dem ursprünglichen Datensatz gleichmässig verteilt sind. Vereinzelt sind für User (z.B. im Bereich 90-150) und Items (z.B Bereicht um 600) dunklere Bereiche erkennbar. In diesen dürften die Ratings höher und Sparsity geringer sein. Die Sparsity beträgt nun auch nur noch etwa 75% und für 25% der möglichen Kombinationen zwischen User und Item wurden Ratings angegeben.

Diese starke Änderung war aber zu erwarten, da wir die User und Items mit den meisten Ratings ausgewählt haben.

2.2.3 Nach der 2. Datenreduktion

MovieLenseCompact_2 <- as(MovieLenseCut_2, "realRatingMatrix")
image(MovieLenseCompact_2,
      xlab = "Items", 
      ylab = "Users", 
      main = "Nach Datenreduktion 2, User-Item Matrix 400 x 700")


sparsity_text(MovieLenseCompact_2)
[1] "Anzahl vorhandene User-Item Rating in 6.35142857142857 %"
[1] "Sparsity der Matrix 93.6485714285714 %"

Für den zweiten Datensatz wird ersichtlich, dass die Ratings gegenüber dem ursprünglichen Datensatz gleichmässig verteilt sind, gegenüber dem ersten Datensatz aber sichtbar weniger Ratings vorhanden sind. Dunklere Bereich, wie bei Datensatz1 sind kaum mehr zu erkennen. Die Sparsity beträgt liegt nun bei 93.6%, sie ist gegenüber dem ersten Datensatz also deutlich angestiegen, liegt aber bereits im Bereich des ursprünglichen Wertes. Dieser Anstieg war zu erwarten, da wir nicht mehr die User und Items mit den meisten Ratings ausgewählt haben, sondern z.B. bei den Usern bei Top 200 angefangen haben.

2.3 Mittlere Kundenratings pro Film vor und nach Datenreduktion,

mean_rating_per_film_viz <- function(movie_df) {
  movie_df %>% 
  group_by(item) %>% 
  summarize(mean_rating_per_film = mean(rating)) %>% 
  ggplot(aes(x = mean_rating_per_film)) + 
  geom_histogram(color = "black", fill = "lightblue", binwidth = 0.1) +
    labs(x = "Ratings", 
       y = "Anzahl", 
       title = "Mittlere Kundenratings Verteilung",
       subtitle = paste("Gesamte Anzahl Kundenratings:", dim(movie_df)[1])) +
    geom_vline(xintercept = mean(movie_df$rating), color = "red", linetype = "dashed", size = 0.5)
}
# Vor reduktion
print(mean_rating_per_film_viz(MovieLenseEDA))
Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
Please use `linewidth` instead.

# nach 1. Redutkion
print(mean_rating_per_film_viz(MovieLenseCut_1))

# nach 2. Reduktion
print(mean_rating_per_film_viz(MovieLenseCut_2))

Die erste Visualisierung zeigt den bereits bekannten Plot mit den mittleren Kundenratings für den gesamten Datensatz. Plot 2 und 3 zeigt die selbe Auswertung für die beiden gekürzten Datensätze. In den Visualisierungen erkennen wir, dass der Mittelwert der Kundenrating für den ursprünglichen, sowie auch für die beiden reduzierten Datensätze, nicht grossartig ändert. Die Mittelwerte befinden sich bei allen im Bereich von 3.5. Was aber erkennbar wird, ist, dass bei der 1. Reduktion die hohe Anzahl Rating bei den natürlichen/ganzzahligen Zahlen weggefallen ist. Weiterhin sind bei allen Visualisierungen erkennbar, dass die meisten Rating im Bereich von 3 bis 4 liegen.

2.4 Für Gruppen: Quantifiziere “Intersection over Union” der Ratings der unterschiedlich reduzierten Datensätze.

intersect_join <- inner_join(MovieLenseCut_1, MovieLenseCut_2, by = c("user", "item"))
intersect_join

union_join <- full_join(MovieLenseCut_1, MovieLenseCut_2, by = c("user", "item"))
union_join

paste("Eine Intersection over Union von", dim(intersect_join)[1] / dim(union_join)[1] * 100, "%, zwischen den beiden reduzierten Datensätzen")
[1] "Eine Intersection over Union von 15.5638434935919 %, zwischen den beiden reduzierten Datensätzen"

Zur Beantwortung dieser Frage haben wir einerseits einen Datensatz mit Daten, die in beiden Datensätzen vorhanden sind erstellt und diesen mit der gesamten Anzahl Daten verglichen. Es zeigt sich, dass es eine Überschneidung von 15.6% zwischen den beiden reduzierten Datensätzen gibt. Dieser eher tiefe Wert überrascht, weil z.B. 50% der User übereinstimmen. Aber aufgrund der hohen Sparsity ist die Überschneidung der Daten viel tiefer.

3 Analyse Ähnlichkeitsmatrix

Aufgabe 3: Erzeuge einen IBCF Recommender und analysiere die Ähnlichkeitsmatrix des trainierten Modelles für den reduzierten Datensatz.

3.1 Zerlege den reduzierten MovieLense Datensatz in ein disjunktes Trainings-und Testdatenset im Verhältnis 4:1,

train_test_split <- function(movie_df, split = 0.8) {
  n <- dim(movie_df)[1]
  n_train <- round(n * split)
  n_test <- n - n_train
  training <- movie_df[1:n_train]
  test <- movie_df[(n_train + 1):n]
  return(list(training, test))
}

train_test_list_1 <- train_test_split(MovieLenseCompact_1)
training_1 <- train_test_list_1[[1]]
test_1 <- train_test_list_1[[2]]
training_1
320 x 700 rating matrix of class ‘realRatingMatrix’ with 53971 ratings.
test_1
80 x 700 rating matrix of class ‘realRatingMatrix’ with 13456 ratings.
train_test_list_2 <- train_test_split(MovieLenseCompact_2)
training_2 <- train_test_list_2[[1]]
test_2 <- train_test_list_2[[2]]
training_2
320 x 700 rating matrix of class ‘realRatingMatrix’ with 14368 ratings.
test_2
80 x 700 rating matrix of class ‘realRatingMatrix’ with 3416 ratings.

Beide reduzierten Datensätze wurden im Verhältnis 4:1, (4 Teile Training und 1 Teil Test) reduziert.

3.2 Trainiere ein IBCF Modell mit 30 Nachbarn und Cosine Similarity

ribcf_1 <- Recommender(training_1, "IBCF", param=list(k= 30, method = "cosine"))
ribcf_1
Recommender of type ‘IBCF’ for ‘realRatingMatrix’ 
learned using 320 users.
ribcf_2 <- Recommender(training_2, "IBCF", param=list(k= 30, method = "cosine"))
ribcf_2
Recommender of type ‘IBCF’ for ‘realRatingMatrix’ 
learned using 320 users.

Es wurden jeweils für beide reduzierten Datensaetze ein IBCF Modell mit 30 Nachbarn und der Cosine Similarity mittels der von Recommenderlab zur Verfügung gestellten Methode trainiert. Die Auswertung bestätigt, dass das Training mittels 320 Usern, resp. 80% der ursprünglichen 400, durchgeführt wurde.

3.3 Bestimme die Verteilung der Filme, welche bei IBCF für paarweise Ähnlichkeitsvergleiche verwendet werden

ribcf_sim_item_df <- function(ribcf) {
  # model
  ribcf_model <- getModel(ribcf)
  # dataframe erstellen
  ribcf_sim_df <- as.data.frame(colSums(ribcf_model$sim > 0))
  # Item als neue Spalte hinzufuegen und Index entfernen
  ribcf_sim_df_ <- cbind(item = rownames(ribcf_sim_df), ribcf_sim_df)
  rownames(ribcf_sim_df_) <- NULL
  # return df
  ribcf_sim_df_
}

ribcf_sim_viz <- function(ribcf_sim_df_, n_reduc) {
    ribcf_sim_df_ %>%
    rename(Anzahl = 2) %>% 
    ggplot(aes(x = Anzahl)) + 
    geom_histogram(binwidth =  1) +
    labs(title = "Verteilung der Ähnlichkeitsvergleiche",
         x = "Anzahl Filme als Nachbar", 
         y = "Anzahl",
         subtitle = paste("ribcf", n_reduc))
}

ribcf_sim_df_1 <- ribcf_sim_item_df(ribcf_1)
ribcf_sim_viz(ribcf_sim_df_1, 1)


ribcf_sim_df_2 <- ribcf_sim_item_df(ribcf_2)
ribcf_sim_viz(ribcf_sim_df_2, 2)

NA
NA

In beiden Histogrammen erkennen wir auf der X Achse die Anzahl Filme die als Nachbar bei einem anderen Film vorkommen. Man erkennt im Plot, dass es wenige Filme gibt, die häufig viele Nachbaren haben. Beide Plots folgene einer ähnlichen Verteilung.

3.4 Bestimme die Filme, die am häufigsten in der Cosine-Ähnlichkeitsmatrix auftauchen und analysiere deren Vorkommen und Ratings im reduzierten Datensatz.

top_10_item_sim <- function(ribcf_sim_df_, n_reduc) {
  result <- ribcf_sim_df_ %>% 
  rename(Anzahl = 2) %>% 
  arrange(desc(Anzahl)) %>% 
  top_n(10)
    
  print(result)
    
  result %>%   
  ggplot(aes(x = Anzahl, y = item)) +
  # arrange desc
  geom_col(alpha = 0.5, color = "black", fill = "limegreen") +
  labs(title = "Top 10 Filme die am häufigsten in der Nachbarschaft andere Filme auftauchen",
       x = "Anzahl Film als Nachbar", 
       y = "Filme",
       subtitle = paste("ribcf", n_reduc))
}
  
top_10_item_sim(ribcf_sim_df_1, 1)
Selecting by Anzahl

top_10_item_sim(ribcf_sim_df_2, 2)
Selecting by Anzahl

Für jeden der beiden Datensätze haben wir einen Dataframe und Plot mit den Filmen, die am häufigsten in der Cosine-Ähnlichkeitsmatrix auftauchen, erstellt. Bei den top 10 Filmen sind keine Gemeinsamkeiten ersichtlich. Die Anzahl der Vorkommen ist aber ähnlich, der höchste Wert ist zwischen 150 und 160 Vorkommen.

4 Implementierung Ähnlichkeitsmatrix

Aufgabe 4 (DIY): Implementiere eine Funktion zur effizienten Berechnung von sparsen Ähnlichkeitsmatrizen für IBCF RS und analysiere die Resultate für 100 zufällig gewählte Filme.

4.1 Implementiere eine Funktion, um (a) für ordinale Ratings effizient

die Cosine Similarity und (b) für binäre Ratings effizient die Jaccard Similarity zu berechnen,

number_user <- 100
number_item <- 100

Diese Variablen haben wir für die Entwicklung der Funktionen verwendet. Wir konnte damit einfach kleinere, z.B. 5, Datensätze slicen.

4.1.1 Cosine Similarity

get_cossim_4 <- function(RatingMatrix, n_user, n_item){
 
  sliced_matrix <- getRatingMatrix(RatingMatrix[1:n_user, 1:n_item])
  
  sliced_matrix_t <- t(sliced_matrix)
  
  temp_sim <- sliced_matrix_t / sqrt(rowSums(sliced_matrix_t ** 2))

  cossim_matrix <- temp_sim %*% t(temp_sim)

  cossim_matrix
}
result_cossim_4 <- get_cossim_4(MovieLense, number_user, number_item)
result_cossim_4[1:20,1:20]
20 x 20 sparse Matrix of class "dgCMatrix"
   [[ suppressing 20 column names ‘Toy Story (1995)’, ‘GoldenEye (1995)’, ‘Four Rooms (1995)’ ... ]]
                                                                                                                                               
Toy Story (1995)                                     1.0000000 0.3786054 0.3557149 0.4085792 0.35406521 0.2106625 0.6507823 0.5128509 0.4809693
GoldenEye (1995)                                     0.3786054 1.0000000 0.1595558 0.4058678 0.33981928 0.1078328 0.2498391 0.2921164 0.2701317
Four Rooms (1995)                                    0.3557149 0.1595558 1.0000000 0.3989039 0.22857516 0.2263010 0.3581228 0.2310704 0.2649610
Get Shorty (1995)                                    0.4085792 0.4058678 0.3989039 1.0000000 0.26745994 0.1328422 0.4245295 0.5445979 0.4781120
Copycat (1995)                                       0.3540652 0.3398193 0.2285752 0.2674599 1.00000000 0.1313064 0.3322003 0.2039381 0.4163740
Shanghai Triad (Yao a yao yao dao waipo qiao) (1995) 0.2106625 0.1078328 0.2263010 0.1328422 0.13130643 1.0000000 0.2130495 0.1805984 0.2092867
Twelve Monkeys (1995)                                0.6507823 0.2498391 0.3581228 0.4245295 0.33220032 0.2130495 1.0000000 0.5179884 0.5991567
Babe (1995)                                          0.5128509 0.2921164 0.2310704 0.5445979 0.20393810 0.1805984 0.5179884 1.0000000 0.6064674
Dead Man Walking (1995)                              0.4809693 0.2701317 0.2649610 0.4781120 0.41637401 0.2092867 0.5991567 0.6064674 1.0000000
Richard III (1995)                                   0.2412952 0.1873510 0.3780578 0.2820925 0.07604487 0.4311379 0.2699059 0.3242348 0.3275021
Seven (Se7en) (1995)                                 0.4553382 0.1632439 0.3843138 0.5195206 0.36995125 0.1093345 0.5694625 0.5399670 0.5360525
Usual Suspects, The (1995)                           0.4651021 0.3486586 0.3794973 0.6074053 0.39453741 0.2776088 0.5844860 0.5715472 0.6633322
Mighty Aphrodite (1995)                              0.5220124 0.2496832 0.3871603 0.3777443 0.19202253 0.3006045 0.5160271 0.4357768 0.4985081
Postino, Il (1994)                                   0.4588155 0.1724141 0.4344255 0.2698077 0.15320397 0.4191717 0.3159025 0.4495279 0.4480904
Mr. Holland's Opus (1995)                            0.6120818 0.2968567 0.3117847 0.3590970 0.39631951 0.1791067 0.5034813 0.3576072 0.5884584
French Twist (Gazon maudit) (1995)                   0.2202742 0.2062550 0.3329636 0.2625611 0.25115377 0.3187884 0.1833778 0.0345436 0.2486767
From Dusk Till Dawn (1996)                           0.3199297 0.3785939 0.2558409 0.3633585 0.32163376 0.2531139 0.2413614 0.2963905 0.3417637
White Balloon, The (1995)                            0.1396962 0.1479453 0.4776651 0.2126344 0.18015094 0.3658636 0.2192261 0.1176950 0.3480472
Antonia's Line (1995)                                0.2946435 0.1047364 0.1690792 0.1032222 0.12753608 0.4662172 0.3078112 0.2350530 0.2710362
Angels and Insects (1995)                            0.1891240 0.1290770 0.4427924 0.1855159 0.15717527 0.4488792 0.1593891 0.2594140 0.3226374
                                                                                                                                                
Toy Story (1995)                                     0.24129522 0.45533821 0.4651021 0.5220124 0.4588155 0.6120818 0.2202742 0.3199297 0.1396962
GoldenEye (1995)                                     0.18735098 0.16324390 0.3486586 0.2496832 0.1724141 0.2968567 0.2062550 0.3785939 0.1479453
Four Rooms (1995)                                    0.37805783 0.38431384 0.3794973 0.3871603 0.4344255 0.3117847 0.3329636 0.2558409 0.4776651
Get Shorty (1995)                                    0.28209250 0.51952065 0.6074053 0.3777443 0.2698077 0.3590970 0.2625611 0.3633585 0.2126344
Copycat (1995)                                       0.07604487 0.36995125 0.3945374 0.1920225 0.1532040 0.3963195 0.2511538 0.3216338 0.1801509
Shanghai Triad (Yao a yao yao dao waipo qiao) (1995) 0.43113793 0.10933445 0.2776088 0.3006045 0.4191717 0.1791067 0.3187884 0.2531139 0.3658636
Twelve Monkeys (1995)                                0.26990593 0.56946246 0.5844860 0.5160271 0.3159025 0.5034813 0.1833778 0.2413614 0.2192261
Babe (1995)                                          0.32423475 0.53996700 0.5715472 0.4357768 0.4495279 0.3576072 0.0345436 0.2963905 0.1176950
Dead Man Walking (1995)                              0.32750209 0.53605254 0.6633322 0.4985081 0.4480904 0.5884584 0.2486767 0.3417637 0.3480472
Richard III (1995)                                   1.00000000 0.35313083 0.3688357 0.3724946 0.4171123 0.1504695 0.1846233 0.3940552 0.4745374
Seven (Se7en) (1995)                                 0.35313083 1.00000000 0.6427527 0.2562351 0.2316937 0.3159148 0.2091273 0.3399173 0.2884724
Usual Suspects, The (1995)                           0.36883567 0.64275270 1.0000000 0.4417965 0.3831814 0.4257209 0.2811128 0.3440000 0.3920784
Mighty Aphrodite (1995)                              0.37249460 0.25623511 0.4417965 1.0000000 0.5845588 0.4709003 0.2874876 0.1293548 0.3692327
Postino, Il (1994)                                   0.41711226 0.23169366 0.3831814 0.5845588 1.0000000 0.4880400 0.2066398 0.2381653 0.5039526
Mr. Holland's Opus (1995)                            0.15046955 0.31591479 0.4257209 0.4709003 0.4880400 1.0000000 0.1586031 0.1868622 0.3185419
French Twist (Gazon maudit) (1995)                   0.18462325 0.20912731 0.2811128 0.2874876 0.2066398 0.1586031 1.0000000 0.2342606 0.4373740
From Dusk Till Dawn (1996)                           0.39405520 0.33991729 0.3440000 0.1293548 0.2381653 0.1868622 0.2342606 1.0000000 0.3360672
White Balloon, The (1995)                            0.47453738 0.28847237 0.3920784 0.3692327 0.5039526 0.3185419 0.4373740 0.3360672 1.0000000
Antonia's Line (1995)                                0.38125745 0.08985732 0.3045318 0.5365988 0.4952783 0.3028259 0.3096346 0.2141239 0.4886175
Angels and Insects (1995)                            0.42364525 0.18246935 0.2980934 0.3282205 0.5172714 0.2431769 0.3815932 0.3420744 0.5303215
                                                                         
Toy Story (1995)                                     0.29464348 0.1891240
GoldenEye (1995)                                     0.10473645 0.1290770
Four Rooms (1995)                                    0.16907917 0.4427924
Get Shorty (1995)                                    0.10322223 0.1855159
Copycat (1995)                                       0.12753608 0.1571753
Shanghai Triad (Yao a yao yao dao waipo qiao) (1995) 0.46621721 0.4488792
Twelve Monkeys (1995)                                0.30781124 0.1593891
Babe (1995)                                          0.23505300 0.2594140
Dead Man Walking (1995)                              0.27103623 0.3226374
Richard III (1995)                                   0.38125745 0.4236452
Seven (Se7en) (1995)                                 0.08985732 0.1824693
Usual Suspects, The (1995)                           0.30453181 0.2980934
Mighty Aphrodite (1995)                              0.53659876 0.3282205
Postino, Il (1994)                                   0.49527834 0.5172714
Mr. Holland's Opus (1995)                            0.30282587 0.2431769
French Twist (Gazon maudit) (1995)                   0.30963462 0.3815932
From Dusk Till Dawn (1996)                           0.21412393 0.3420744
White Balloon, The (1995)                            0.48861751 0.5303215
Antonia's Line (1995)                                1.00000000 0.3100373
Angels and Insects (1995)                            0.31003735 1.0000000

Mit der erstellten Funktion haben wir für den gesamten MovieLense Datensatz die Cosine Similarity Matrix berechnet. Um das Resultat lesbar darzustellen, zeigen wir hier nur die ersten fünf Item. Bei der Analyse der ersten 20 Items wurde ersichtlich, dass die Werte zwischen 0 und 1 liegen. Negative Similarities sind nicht ersichtlich. Wie mit dir besprochen und hergeleitet, ist das aber verständlich, da aufgrund der nicht-negativen Ratings der maximale Winkel 90° beträgt. Hätten wir mit normierten Ratings gearbeitet, wären auch negative Werte aufgetreten.

4.1.2 Jaccard Similarity

get_jaccardsim_4 <- function(RatingMatrix, n_user, n_item){
  
  sliced_matrix_bin <- as(binarize(RatingMatrix[1:n_user, 1:n_item], minRating=4), "matrix")
  
  sliced_matrix_bin_t <- t(sliced_matrix_bin)
  
  matrix_corssprod <- tcrossprod(sliced_matrix_bin_t)
  
  im <- which(matrix_corssprod > 0, arr.ind=TRUE)
  b <- rowSums(sliced_matrix_bin_t)
  Aim <- matrix_corssprod[im]
  
  J = sparseMatrix(
            i = im[,1],
            j = im[,2],
            x = Aim / (b[im[,1]] + b[im[,2]] - Aim),
            dims = dim(matrix_corssprod)
      )
  
  J <- data.matrix(J)
  
  J
}
jaccardsim_4 <- get_jaccardsim_4(MovieLense, number_user, number_item)
jaccardsim_4[1:5, 1:5]
           [,1]       [,2]       [,3]      [,4]      [,5]
[1,] 1.00000000 0.05128205 0.05128205 0.1304348 0.1052632
[2,] 0.05128205 1.00000000 0.00000000 0.0625000 0.0000000
[3,] 0.05128205 0.00000000 1.00000000 0.0625000 0.0000000
[4,] 0.13043478 0.06250000 0.06250000 1.0000000 0.1250000
[5,] 0.10526316 0.00000000 0.00000000 0.1250000 1.0000000

Bei der Jaccard Similarity sind wiederum nur positive Werte ersichtlich. Eine Auswertung dieser geplotteten Werte ergibt, dass sehr wenige Werte über 0.3 liegen. Die Ähnlichkeit dieser ersten 10 Filme gegenüber den ersten 100 anderen Items ist also eher gering.

4.2 Vergleiche deine Implementierung der Cosine-basierten

Ähnlichkeitsmatrix für ordinale Ratings mit der via recommenderlab und einem anderen R-Paket erzeugten Ähnlichkeitsmatrix,

4.2.1 Vergleich Cosine Similiarty mit Recommenderlab

#recom_simcosin_4 <- as.matrix(similarity(normalize(MovieLense[1:number_user, 1:number_item]), which = "items", method = "cosine"))
recom_simcosin_4 <- as.matrix(similarity(MovieLense[1:number_user, 1:number_item], which = "items", method = "cosine"))

recom_simcosin_4[1:5,1:5]
                  Toy Story (1995) GoldenEye (1995) Four Rooms (1995) Get Shorty (1995) Copycat (1995)
Toy Story (1995)                NA        0.9821980         0.9308431         0.9664688      0.9730499
GoldenEye (1995)         0.9821980               NA         0.9455211         0.9598695      0.9629100
Four Rooms (1995)        0.9308431        0.9455211                NA         0.9687050      0.9472136
Get Shorty (1995)        0.9664688        0.9598695         0.9687050                NA      0.9368489
Copycat (1995)           0.9730499        0.9629100         0.9472136         0.9368489             NA
result_cossim_4_scaled <- 1 / 2 * (result_cossim_4 + 1)

result_cossim_4_scaled[1:5,1:5]
5 x 5 Matrix of class "dgeMatrix"
                  Toy Story (1995) GoldenEye (1995) Four Rooms (1995) Get Shorty (1995) Copycat (1995)
Toy Story (1995)         1.0000000        0.6893027         0.6778574         0.7042896      0.6770326
GoldenEye (1995)         0.6893027        1.0000000         0.5797779         0.7029339      0.6699096
Four Rooms (1995)        0.6778574        0.5797779         1.0000000         0.6994520      0.6142876
Get Shorty (1995)        0.7042896        0.7029339         0.6994520         1.0000000      0.6337300
Copycat (1995)           0.6770326        0.6699096         0.6142876         0.6337300      1.0000000

Für den Vergleich unserer Implementation mit der von Recommenderlab haben wir festgestellt, dass Recommenderlab die Resultate auf Werte zwischen 0 und 1 normiert. Wir haben diese Normierung auch auf unser Resultat durchgeführt. Oben ist das Resultat von Recommenderlab für die ersten fünf Filme ersichtlich, darunter unsere, normierte Implementation. Es lässt sich feststellen, dass die Werte von Recommenderlab sehr hoch, in der Nähe von 1 liegen. Unsere hingegen reichen etwa von 0.5 bis 1. Wir konnten die Ursache der Differenz nicht feststellen, es wäre aber ein Zufall, wenn die Ähnlichkeiten wirklich so hoch wären. Unsere Bandbreite, von 0.5 (resp. unnormiert 0) bis 1 ist deshalb realistischer.

4.2.2 Vergleich Cosine Similiarity mit anderem R-Paket

library(lsa)

rec_simMat <- similarity(MovieLenseCompact_1[,1:5], which = "items")
rec_simMat
                                    101 Dalmatians (1996) 12 Angry Men (1957) 187 (1997) 2 Days in the Valley (1996)
12 Angry Men (1957)                             0.9491014                                                           
187 (1997)                                      0.9377585           0.9951661                                       
2 Days in the Valley (1996)                     0.9424520           0.9908561  0.9728694                            
20,000 Leagues Under the Sea (1954)             0.9636541           0.9846314  0.9824506                   0.9745629
result_cossim_4_scaled[1:5,1:5]
5 x 5 Matrix of class "dgeMatrix"
                  Toy Story (1995) GoldenEye (1995) Four Rooms (1995) Get Shorty (1995) Copycat (1995)
Toy Story (1995)         1.0000000        0.6893027         0.6778574         0.7042896      0.6770326
GoldenEye (1995)         0.6893027        1.0000000         0.5797779         0.7029339      0.6699096
Four Rooms (1995)        0.6778574        0.5797779         1.0000000         0.6994520      0.6142876
Get Shorty (1995)        0.7042896        0.7029339         0.6994520         1.0000000      0.6337300
Copycat (1995)           0.6770326        0.6699096         0.6142876         0.6337300      1.0000000

Wir konnten keinen direkten Vergleich mit der Implementation von LSA machen, weil sie die Items anders sortieren und wir deshalb keine Übereinstimmende Items erhalten haben. Auch wenn wir die ersten 10 Items angezeigt haben, konnten wir keine Übereinstimmung von Items finden. Was aber auffällt ist, dass LSA auch sehr hohe Similarities, im Bereich von 1, berechnet hat.

4.3 Vergleiche deine mittels Cosine Similarity erzeugten Ähnlichkeitsmatrix für ordinale Ratings mit der Jaccard-basierten Ähnlichkeitsmatrix für binäre Ratings.

print("Cosine Similarity")
[1] "Cosine Similarity"
result_cossim_4[1:5,1:5]
5 x 5 sparse Matrix of class "dgCMatrix"
                  Toy Story (1995) GoldenEye (1995) Four Rooms (1995) Get Shorty (1995) Copycat (1995)
Toy Story (1995)         1.0000000        0.3786054         0.3557149         0.4085792      0.3540652
GoldenEye (1995)         0.3786054        1.0000000         0.1595558         0.4058678      0.3398193
Four Rooms (1995)        0.3557149        0.1595558         1.0000000         0.3989039      0.2285752
Get Shorty (1995)        0.4085792        0.4058678         0.3989039         1.0000000      0.2674599
Copycat (1995)           0.3540652        0.3398193         0.2285752         0.2674599      1.0000000
print("jaccard-basiert")
[1] "jaccard-basiert"
jaccardsim_4[1:5, 1:5]
           [,1]       [,2]       [,3]      [,4]      [,5]
[1,] 1.00000000 0.05128205 0.05128205 0.1304348 0.1052632
[2,] 0.05128205 1.00000000 0.00000000 0.0625000 0.0000000
[3,] 0.05128205 0.00000000 1.00000000 0.0625000 0.0000000
[4,] 0.13043478 0.06250000 0.06250000 1.0000000 0.1250000
[5,] 0.10526316 0.00000000 0.00000000 0.1250000 1.0000000

Zwischen Similarity basierend auf Cosine und Jaccard sind deutliche Unterschiede ersichtlich. Bei Cosine betragen die meisten Werte zwischen 0.15 und 0.40. Im Gegensatz betrage die meisten Werte bei Jaccard um oder den Wert Null. Das dürfte daran liegen, dass aufgrund der binären Codierung der Ratings, weniger Übereinstimmungen bestehen.

5 Analyse Top-N Listen - IBCF vs UBCF

Aufgabe 5: Vergleiche und diskutiere Top-N Empfehlungen von IBCF und UBCF Modellen mit 30 Nachbarn und Cosine Similarity für den reduzierten Datensatz. ## 5.1 Berechne Top-15 Empfehlungen für Testkunden mit IBCF und UBCF ### 5.1.1 ribcf & rubcf Modell trainieren

ribcf_1 <- Recommender(training_1, "IBCF", param=list(k= 30, method = "cosine"))
ribcf_1
Recommender of type ‘IBCF’ for ‘realRatingMatrix’ 
learned using 320 users.
rubcf_1 <- Recommender(training_1, "UBCF", param=list(nn= 30, method = "cosine"))
rubcf_1
Recommender of type ‘UBCF’ for ‘realRatingMatrix’ 
learned using 320 users.
ribcf_2 <- Recommender(training_2, "IBCF", param=list(k= 30, method = "cosine"))
ribcf_2
Recommender of type ‘IBCF’ for ‘realRatingMatrix’ 
learned using 320 users.
rubcf_2 <- Recommender(training_2, "UBCF", param=list(nn= 30, method = "cosine"))
rubcf_2
Recommender of type ‘UBCF’ for ‘realRatingMatrix’ 
learned using 320 users.

Es wurden für beide reduzierten Datensätze jeweils ein ibcf und ubcf Recommender erstellt.

5.1.2 Model Predicitions erstellen

ribcftopNList_1 <- predict(ribcf_1, test_1, n=15)
ribcftopNList_1
Recommendations as ‘topNList’ with n = 15 for 80 users. 
rubcftopNList_1 <- predict(rubcf_1, test_1, n=15)
rubcftopNList_1
Recommendations as ‘topNList’ with n = 15 for 80 users. 
ribcftopNList_2 <- predict(ribcf_2, test_2, n=15)
ribcftopNList_2
Recommendations as ‘topNList’ with n = 15 for 80 users. 
rubcftopNList_2 <- predict(rubcf_2, test_2, n=15)
rubcftopNList_2
Recommendations as ‘topNList’ with n = 15 for 80 users. 

Nun wurden für beide Datensätze Predictions mit n = 15 und für 80 User berechnet.

5.1.3 Ausgabe von einer Prediction

# ausgabe von einem output
as(ribcftopNList_1, "list")[1:5]
$`0`
 [1] "Strictly Ballroom (1992)"                                   "Like Water For Chocolate (Como agua para chocolate) (1992)"
 [3] "Casablanca (1942)"                                          "In the Company of Men (1997)"                              
 [5] "Love Bug, The (1969)"                                       "My Left Foot (1989)"                                       
 [7] "African Queen, The (1951)"                                  "Fantasia (1940)"                                           
 [9] "Fear (1996)"                                                "Sphere (1998)"                                             
[11] "Dial M for Murder (1954)"                                   "Dead Man Walking (1995)"                                   
[13] "Citizen Kane (1941)"                                        "Smoke (1995)"                                              
[15] "What's Eating Gilbert Grape (1993)"                        

$`1`
 [1] "2 Days in the Valley (1996)"          "Adventures of Robin Hood, The (1938)" "Alice in Wonderland (1951)"          
 [4] "American in Paris, An (1951)"         "Being There (1979)"                   "Bringing Up Baby (1938)"             
 [7] "Cinema Paradiso (1988)"               "Crash (1996)"                         "Dead Man Walking (1995)"             
[10] "Ed Wood (1994)"                       "Grand Day Out, A (1992)"              "Hunt for Red October, The (1990)"    
[13] "Jackie Chan's First Strike (1996)"    "Jungle Book, The (1994)"              "Madness of King George, The (1994)"  

$`2`
 [1] "Time to Kill, A (1996)"            "City Hall (1996)"                  "City of Lost Children, The (1995)"
 [4] "In the Company of Men (1997)"      "Independence Day (ID4) (1996)"     "M*A*S*H (1970)"                   
 [7] "Forrest Gump (1994)"               "Braveheart (1995)"                 "Dave (1993)"                      
[10] "Dead Poets Society (1989)"         "Mr. Holland's Opus (1995)"         "Cool Hand Luke (1967)"            
[13] "In the Line of Fire (1993)"        "Ghost (1990)"                      "Blade Runner (1982)"              

$`3`
 [1] "Bob Roberts (1992)"                           "Clerks (1994)"                                "Cool Hand Luke (1967)"                       
 [4] "Gandhi (1982)"                                "His Girl Friday (1940)"                       "Ice Storm, The (1997)"                       
 [7] "Pink Floyd - The Wall (1982)"                 "Postino, Il (1994)"                           "Rebel Without a Cause (1955)"                
[10] "Willy Wonka and the Chocolate Factory (1971)" "Shine (1996)"                                 "Seven (Se7en) (1995)"                        
[13] "Grosse Pointe Blank (1997)"                   "Three Colors: Red (1994)"                     "Boot, Das (1981)"                            

$`4`
 [1] "Monty Python's Life of Brian (1979)" "Henry V (1989)"                      "As Good As It Gets (1997)"          
 [4] "In the Company of Men (1997)"        "Junior (1994)"                       "Renaissance Man (1994)"             
 [7] "Cinema Paradiso (1988)"              "Dumbo (1941)"                        "White Squall (1996)"                
[10] "Boot, Das (1981)"                    "Strictly Ballroom (1992)"            "When Harry Met Sally... (1989)"     
[13] "Forbidden Planet (1956)"             "Amadeus (1984)"                      "American in Paris, An (1951)"       

Dies ist eine Übersicht der Empfehlungen für die ersten 5 User. Wie erfordert, wurden jeweils 15 Empfehlungen generiert. Auf den ersten Blick werden viele unterschiedlichen Filme empfohlen.

5.2 Vergleiche die Top-15 Empfehlungen und deren Verteilung und diskutiere Gemeinsamkeiten und Unterschiede zwischen IBCF und UBCF für alle Testkunden.

# df funktion erstellen
topN_df <- function(topNList){
  counts <- table(unlist(as.array(as(topNList, "list"))))
  df <- data.frame(Movie = names(counts), Count = unname(counts)) %>%
    select("Movie", "Count.Freq") %>%
    rename("Count" = "Count.Freq") %>%
    arrange(desc(Count))  
  df
}

# alle dfs erstellen
ribcftopN_df_1 <- topN_df(ribcftopNList_1)
ribcftopN_df_1
ribcftopN_df_2 <- topN_df(ribcftopNList_2)
ribcftopN_df_2

rubcftopN_df_1 <- topN_df(rubcftopNList_1)
rubcftopN_df_1
rubcftopN_df_2 <- topN_df(rubcftopNList_2)
rubcftopN_df_2
NA

Die ersten beiden Tabellen stellen die Empfehlungen und deren Anzahl basierend auf IBCF für die beiden Datensätze dar. In den Top 10 Empfehlungen sind sehr unterschiedliche Empfehlungen, es gibt kaum Überschneidungen. Für den ersten Datensatz wird ein Film maximal 11 mal, im zweiten maximal 15 mal empfohlen. Beim ersten Datensatz werden insgesamt 487 Filme und beim zweiten 407 empfohlen.

Die letzten beiden Tabellen stellen die Empfehlungen basierend auf UBCF für die beiden Datensätze dar. Die Top 10 Filme sind wieder sehr unterschiedlich. Grosse Unterschiede gibt es auch bei der Anzahl Vorkommen der Top Filme. Für den ersten Datensatz werden sie bis zu 30 mal empfohlen, während es beim zweiten maximal 14 mal war. Auch liegt die Anzahl Empfehlungen mit 301 vs 392 weit auseinander.

Für weitere Informationen visualisieren wir nun auch die Top Empfehlungen.

5.2.1 Verteilungen visualisieren

# funktion zur Visualisierung
top15_df_visualize <- function(topNList, subtitle){
  topNList %>% head(15) %>% 
    ggplot(aes(x = reorder(Movie, Count), y = Count)) +
    geom_bar(stat = "identity", fill = "limegreen", alpha = 0.5, color = "black") +
    coord_flip() +
    labs(x = "Movie", 
         y = "Anzahl", 
         title = "Top-15 Empfehlungen",
         subtitle = subtitle)
}

grid.arrange(top15_df_visualize(ribcftopN_df_1, "ribcf 1"),
             top15_df_visualize(rubcftopN_df_1, "rubcf 1"),
             ncol = 2)



grid.arrange(top15_df_visualize(ribcftopN_df_2, "ribcf 2"),
             top15_df_visualize(rubcftopN_df_2, "rubcf 2"),
             ncol = 2)

Dank der library gridExtra können wir die beiden Datensätze nebeneinander darstellen. Ersichtlich wird, wie schnell die Anzahl Empfehlungen pro Film abnimmt. In der ersten Lasche, IBCF, sieht man, dass die Anzahl linear abnimmt, nachdem die ersten fünf Filme gleich häufig empfohlen werden. Hingegen nehmen die Anzahl im zweiten Datensatz (Grafik rechts) zuerst schnell, bis etwa zum Niveau des ersten Datensatzes, dann linear ab. Bei UBCF, in der zweiten Lasche, nimmt die Anzahl bei beiden Datensätzen linear ab.

Die erwähnte Behauptung “Recommender Systeme machen für alle Nutzer die gleichen Empfehlungen” kann dank der Tabellen und Histogramme verworfen werden. Es werden viele unterschiedliche Filme empfohlen, vieleviele Filme werden nur wenigen Usern (<4) empfohlen.

6 Analyse Top-N Listen - Ratings

Aufgabe 6: Untersuche den Einfluss von Ratings (ordinale vs binäre Ratings) und Modelltyp (IBCF vs UBCF) auf Top-N Empfehlungen für den reduzierten Datensatz. Vergleiche den Anteil übereinstimmender Empfehlungen der Top-15 Liste für ## 6.1 IBCF vs UBCF, beide mit ordinalem Rating und Cosine Similarity für alle Testkunden,

compare_ibcf_ubcf <- function(ibcf, ubcf) {
  print(paste("Anzahl IBCF:", nrow(ibcf)))
  print(paste("Anzahl UBCF:", nrow(ubcf)))

  IntersectordRatCosine <- intersect(ibcf$Movie, ubcf$Movie)

  print(paste("Anzahl gemeinsame Empfehlungen:", length(IntersectordRatCosine)))
  print(paste("Anteil IBCF:", length(IntersectordRatCosine) / nrow(ibcf) * 100))
  print(paste("Anteil UBCF:", length(IntersectordRatCosine) / nrow(ubcf) * 100))
}

print("Erste Datenreduktion")
[1] "Erste Datenreduktion"
compare_ibcf_ubcf(ribcftopN_df_1, rubcftopN_df_1)
[1] "Anzahl IBCF: 487"
[1] "Anzahl UBCF: 301"
[1] "Anzahl gemeinsame Empfehlungen: 231"
[1] "Anteil IBCF: 47.4332648870637"
[1] "Anteil UBCF: 76.7441860465116"
print("Zweite Datenreduktion")
[1] "Zweite Datenreduktion"
compare_ibcf_ubcf(ribcftopN_df_2, rubcftopN_df_2)
[1] "Anzahl IBCF: 407"
[1] "Anzahl UBCF: 392"
[1] "Anzahl gemeinsame Empfehlungen: 226"
[1] "Anteil IBCF: 55.5282555282555"
[1] "Anteil UBCF: 57.6530612244898"

Erste Datenreduktion: Für IBCF werden 487 und UBCF 301 Filme empfohlen, dabei gibt es eine Übereinstimmung von 231 Filmen. Das entsprechen bei IBCF 47.5% und bei UBCF 76.7%. Zweite Datenreduktion: Für IBCF werden 407 und UBCF 392 Filme empfohlen, dabei gibt es eine Übereinstimmung von 226 Filmen. Das entsprechen bei IBCF 55% und bei UBCF 57%.

Insgesamt generieren also beide Methoden ähnliche Empfehlungen, rund die Hälfte bis 3/4 der Empfehlungen generiert auch die andere Methode. Auffällig ist hingegen beim ersten Datensatz, dass IBCF viel mehr Filme empfiehlt, während es beim zweiten etwa gleich viel sind.

Beim zweiten Datensatz ist auch der Anteil an Gemeinsamkeiten jeweils bei rund 55% und damit ausgeglichener als im ersten Datensatz. Ich kann mir vorstellen, dass das daran liegt, dass beim zweiten Datensatz die Sparsity der Matrix höher ist und damit mehr Spielraum offen ist.

6.2 IBCF vs UBCF, beide mit binärem Rating und Jaccard Similarity für alle Testkunden,

training_bin_1 <- binarize(training_1, minRating = 4)
test_bin_1 <- binarize(test_1, minRating = 4)

training_bin_2 <- binarize(training_2, minRating = 4)
test_bin_2 <- binarize(test_2, minRating = 4)

ribcf_bin_1 <- Recommender(training_bin_1, "IBCF", param=list(k= 30, method = "jaccard"))
ribcf_bin_1
Recommender of type ‘IBCF’ for ‘binaryRatingMatrix’ 
learned using 320 users.
rubcf_bin_1 <- Recommender(training_bin_1, "UBCF", param=list(nn= 30, method = "jaccard"))
rubcf_bin_1
Recommender of type ‘UBCF’ for ‘binaryRatingMatrix’ 
learned using 320 users.
ribcf_bin_2 <- Recommender(training_bin_2, "IBCF", param=list(k= 30, method = "jaccard"))
ribcf_bin_2
Recommender of type ‘IBCF’ for ‘binaryRatingMatrix’ 
learned using 320 users.
rubcf_bin_2 <- Recommender(training_bin_2, "UBCF", param=list(nn= 30, method = "jaccard"))
rubcf_bin_2
Recommender of type ‘UBCF’ for ‘binaryRatingMatrix’ 
learned using 320 users.
ribcftopNList_bin_1 = predict(ribcf_bin_1, test_bin_1, n=15)
ribcftopNList_bin_1
Recommendations as ‘topNList’ with n = 15 for 80 users. 
rubcftopNList_bin_1 = predict(rubcf_bin_1, test_bin_1, n=15)
rubcftopNList_bin_1
Recommendations as ‘topNList’ with n = 15 for 80 users. 
ribcftopNList_bin_2 = predict(ribcf_bin_2, test_bin_2, n=15)
ribcftopNList_bin_2
Recommendations as ‘topNList’ with n = 15 for 80 users. 
rubcftopNList_bin_2 = predict(rubcf_bin_2, test_bin_2, n=15)
rubcftopNList_bin_2
Recommendations as ‘topNList’ with n = 15 for 80 users. 
ribcftopN_df_bin_1 <- topN_df(ribcftopNList_bin_1)
ribcftopN_df_bin_1

ribcftopN_df_bin_2 <- topN_df(ribcftopNList_bin_2)
ribcftopN_df_bin_2

rubcftopN_df_bin_1 <- topN_df(rubcftopNList_bin_1)
rubcftopN_df_bin_1

rubcftopN_df_bin_2 <- topN_df(rubcftopNList_bin_2)
rubcftopN_df_bin_2

Diese Auswertung entspricht der, der vorherigen Aufgabe, nur dass dieses mal mit binären Ratings und Jaccard Similarity gearbeitet wurde. Es wird auch hier ersichtlich, dass die Empfehlungen sehr unterschiedlich sind. Bei IBCF (erste zwei Tabellen) werden für den ersten Datensatz die Top Filme sehr viel häufiger (39 mal vs 16 mal) empfohlen. Das gleiche Muster, wenn aber schwächer, ist bei den UBCF ersichtlich.

print("Erste Datenreduktion binaer")
[1] "Erste Datenreduktion binaer"
compare_ibcf_ubcf(ribcftopN_df_bin_1, rubcftopN_df_bin_1)
[1] "Anzahl IBCF: 87"
[1] "Anzahl UBCF: 447"
[1] "Anzahl gemeinsame Empfehlungen: 6"
[1] "Anteil IBCF: 6.89655172413793"
[1] "Anteil UBCF: 1.34228187919463"
print("Zweite Datenreduktion binaer")
[1] "Zweite Datenreduktion binaer"
compare_ibcf_ubcf(ribcftopN_df_bin_2, rubcftopN_df_bin_2)
[1] "Anzahl IBCF: 411"
[1] "Anzahl UBCF: 519"
[1] "Anzahl gemeinsame Empfehlungen: 296"
[1] "Anteil IBCF: 72.0194647201946"
[1] "Anteil UBCF: 57.0327552986513"

Im Gegensatz zur vorherigen Aufgabe und den zweiten Datensatz, gibt es für den ersten fast keine gemeinsame Empfehlungen. Es fällt auch auf, dass für IBCF nur 87 Filme empfohlen werden. Diese Auswertung wurde mit minRating 4 für die binäre Klassifizierung berechnet. Mit Rating 3 sieht dieser Sachverhalt ähnlich aus, bei minRating 5 ist die Übereinstimmung aber wieder im normalen Bereich. Wieso minRating 3 und 4 so tiefe Übereinstimmungen generiert haben, können wir nicht nachvollziehe. Dass minRating 5 aber bessere Resultate generiert, liegt daran, dass nun nur noch wenige Items als 1 klassifiziert werden und damit weniger Filme zur Empfehlung zur Verfügung stellen.

6.3 UBCF mit ordinalem (Cosine Similarity) vs UBCF mit binärem Rating (Jaccard Similarity) für alle Testkunden.

compare_ubcf <- function(ibcf, ubcf) {
  print(paste("Anzahl UBCF ord:", nrow(ibcf)))
  print(paste("Anzahl UBCF bin:", nrow(ubcf)))

  IntersectordRatCosine <- intersect(ibcf$Movie, ubcf$Movie)

  print(paste("Anzahl gemeinsame Empfehlungen:", length(IntersectordRatCosine)))
  print(paste("Anteil UBCF ord:", length(IntersectordRatCosine) / nrow(ibcf) * 100))
  print(paste("Anteil UBCF bin:", length(IntersectordRatCosine) / nrow(ubcf) * 100))
}

Erstellung der Funktion und Berechnung des Resultats

print("Erste Datenreduktion")
[1] "Erste Datenreduktion"
compare_ubcf(rubcftopN_df_1, rubcftopN_df_bin_1)
[1] "Anzahl UBCF ord: 301"
[1] "Anzahl UBCF bin: 447"
[1] "Anzahl gemeinsame Empfehlungen: 207"
[1] "Anteil UBCF ord: 68.7707641196013"
[1] "Anteil UBCF bin: 46.3087248322148"
print("Zweite Datenreduktion")
[1] "Zweite Datenreduktion"
compare_ubcf(rubcftopN_df_2, rubcftopN_df_bin_2)
[1] "Anzahl UBCF ord: 392"
[1] "Anzahl UBCF bin: 519"
[1] "Anzahl gemeinsame Empfehlungen: 301"
[1] "Anteil UBCF ord: 76.7857142857143"
[1] "Anteil UBCF bin: 57.9961464354528"

Beim Vergleich von UBCF mit ordinalem und binärem Rating werden wieder mehr übereinstimmende Filme empfohlen. Für den ersten Datensatz werden 207 Filme bei beiden Modellen und beim zweiten Datensatz 301 übereinstimmende Filme empfohlen. Da bei beiden Datensätzen mit ordinalem Rating weniger Empfehlungen generiert werden, ist der Anteil Übereinstimmungen bei ordinalen Ratings entsprechend höher.

7 Analyse Top-N Listen - IBCF vs SVD

Aufgabe 7: Vergleiche Memory-based IBCF und Modell-based SVD Recommenders bezüglich Überschneidung ihrer Top-N Empfehlungen für die User-Item Matrix des reduzierten Datensatzes (Basis: reduzierter Datensatz, IBCF mit 30 Nachbarn und Cosine Similarity). Vergleiche wie sich der Anteil übereinstimmender Empfehlungen der Top-15 Liste für IBCF vs verschiedene SVD Modelle verändert, wenn die Anzahl der Singulärwerte für SVD von 10 auf 20, 30, 40, 50 verändert wird.

# Funktion fuer SVD Model
generate_SVD_topN_recomm <- function(train, test, svd_value = ksvd){
    recom_model <- Recommender(train, "SVD", param=list(k= svd_value))
    top_n_recom <- predict(recom_model, test, n=15)
  top_n_recom
}

# Funktion fuer verschiedene N
generate_SVD_topN_lists <- function(train, test, N_values) {
  rsvd_topN_lists <- list()
  for (i in 1:length(N_values)) {
    N <- N_values[i]
    list_name <- paste0("rsvd", N, "topNList")
    rsvd_topN_lists[[list_name]] <- generate_SVD_topN_recomm(train, test, N)
  }
  rsvd_topN_lists
}

Funktion zur Berechnung des Resultats

N_values <- c(10, 20, 30, 40, 50)
rsvd_topN_lists_1 <- generate_SVD_topN_lists(training_1, test_1, N_values)
print("Erster Datensatz")
[1] "Erster Datensatz"
rsvd_topN_lists_1
$rsvd10topNList
Recommendations as ‘topNList’ with n = 15 for 80 users. 

$rsvd20topNList
Recommendations as ‘topNList’ with n = 15 for 80 users. 

$rsvd30topNList
Recommendations as ‘topNList’ with n = 15 for 80 users. 

$rsvd40topNList
Recommendations as ‘topNList’ with n = 15 for 80 users. 

$rsvd50topNList
Recommendations as ‘topNList’ with n = 15 for 80 users. 
rsvd_topN_lists_2 <- generate_SVD_topN_lists(training_2, test_2, N_values)
print("Zweiter Datensatz")
[1] "Zweiter Datensatz"
rsvd_topN_lists_2
$rsvd10topNList
Recommendations as ‘topNList’ with n = 15 for 80 users. 

$rsvd20topNList
Recommendations as ‘topNList’ with n = 15 for 80 users. 

$rsvd30topNList
Recommendations as ‘topNList’ with n = 15 for 80 users. 

$rsvd40topNList
Recommendations as ‘topNList’ with n = 15 for 80 users. 

$rsvd50topNList
Recommendations as ‘topNList’ with n = 15 for 80 users. 
generate_topN_dfs <- function(rsvd_topN_lists) {
  topN_dfs <- list()
  
  for (i in 1:length(rsvd_topN_lists)) {
    list_name <- names(rsvd_topN_lists)[i]
    df_name <- paste0(list_name, "_df")
    topN_dfs[[df_name]] <- topN_df(rsvd_topN_lists[[i]])
  }
  
  topN_dfs
}

topN_df_svd_1 <- generate_topN_dfs(rsvd_topN_lists_1)
print("Erster Datensatz")
[1] "Erster Datensatz"
topN_df_svd_1
$rsvd10topNList_df

$rsvd20topNList_df

$rsvd30topNList_df

$rsvd40topNList_df

$rsvd50topNList_df
topN_df_svd_2 <- generate_topN_dfs(rsvd_topN_lists_2)
print("Zweiter Datensatz")
[1] "Zweiter Datensatz"
topN_df_svd_2
$rsvd10topNList_df

$rsvd20topNList_df

$rsvd30topNList_df

$rsvd40topNList_df

$rsvd50topNList_df
NA

Die ersten fünf Tabellen sind die Resultate für den ersten Datensatz. Es handelt sich aufsteigend um die Anzahl Singulärwerte von 10 bis 50. Die letzten fünf Tabellen beinhalten das gleiche Resultat, einfach für den zweiten Datensatz. Diese Auswertung wird noch nicht für die Beantwortung der Aufgabe verwendet, sondern gab uns einen ersten Überblick über die Resultate

compare_ibcf_svd <- function(ribcf, svd, svd_value) {
  intersect <- intersect(ribcf$Movie, svd$Movie)
  print(paste("Anzahl gemeinsame Empfehlungen SVD", svd_value, ":", length(intersect)))
  print(paste("Gemeinsamer relativer Anteil für Anzahl Singulärwerte", svd_value, ":", length(intersect) / nrow(ribcf) * 100))
}

print("Erster Datensatz")
[1] "Erster Datensatz"
compare_ibcf_svd(ribcftopN_df_1, topN_df_svd_1$rsvd10topNList_df, 10)
[1] "Anzahl gemeinsame Empfehlungen SVD 10 : 76"
[1] "Gemeinsamer relativer Anteil für Anzahl Singulärwerte 10 : 15.605749486653"
compare_ibcf_svd(ribcftopN_df_1, topN_df_svd_1$rsvd20topNList_df, 20)
[1] "Anzahl gemeinsame Empfehlungen SVD 20 : 86"
[1] "Gemeinsamer relativer Anteil für Anzahl Singulärwerte 20 : 17.6591375770021"
compare_ibcf_svd(ribcftopN_df_1, topN_df_svd_1$rsvd30topNList_df, 30)
[1] "Anzahl gemeinsame Empfehlungen SVD 30 : 93"
[1] "Gemeinsamer relativer Anteil für Anzahl Singulärwerte 30 : 19.0965092402464"
compare_ibcf_svd(ribcftopN_df_1, topN_df_svd_1$rsvd40topNList_df, 40)
[1] "Anzahl gemeinsame Empfehlungen SVD 40 : 105"
[1] "Gemeinsamer relativer Anteil für Anzahl Singulärwerte 40 : 21.5605749486653"
compare_ibcf_svd(ribcftopN_df_1, topN_df_svd_1$rsvd50topNList_df, 50)
[1] "Anzahl gemeinsame Empfehlungen SVD 50 : 117"
[1] "Gemeinsamer relativer Anteil für Anzahl Singulärwerte 50 : 24.0246406570842"
print("Zweiter Datensatz")
[1] "Zweiter Datensatz"
compare_ibcf_svd(ribcftopN_df_2, topN_df_svd_2$rsvd10topNList_df, 10)
[1] "Anzahl gemeinsame Empfehlungen SVD 10 : 10"
[1] "Gemeinsamer relativer Anteil für Anzahl Singulärwerte 10 : 2.45700245700246"
compare_ibcf_svd(ribcftopN_df_2, topN_df_svd_2$rsvd20topNList_df, 20)
[1] "Anzahl gemeinsame Empfehlungen SVD 20 : 20"
[1] "Gemeinsamer relativer Anteil für Anzahl Singulärwerte 20 : 4.91400491400491"
compare_ibcf_svd(ribcftopN_df_2, topN_df_svd_2$rsvd30topNList_df, 30)
[1] "Anzahl gemeinsame Empfehlungen SVD 30 : 22"
[1] "Gemeinsamer relativer Anteil für Anzahl Singulärwerte 30 : 5.4054054054054"
compare_ibcf_svd(ribcftopN_df_2, topN_df_svd_2$rsvd40topNList_df, 40)
[1] "Anzahl gemeinsame Empfehlungen SVD 40 : 27"
[1] "Gemeinsamer relativer Anteil für Anzahl Singulärwerte 40 : 6.63390663390663"
compare_ibcf_svd(ribcftopN_df_2, topN_df_svd_2$rsvd50topNList_df, 50)
[1] "Anzahl gemeinsame Empfehlungen SVD 50 : 27"
[1] "Gemeinsamer relativer Anteil für Anzahl Singulärwerte 50 : 6.63390663390663"

Für den ersten Datensatz werden bei SVD Wert 10 76 gemeinsame Empfehlungen mit IBCF generiert. Dies entspricht einem Anteil von 15.6% der Empfehlungen vom SVD Modell. Bis zur Anzahl von 50 Singulärwerten steigt die Anzahl gemeinsamer Empfehlungen auf 117, was einem Anteil von 24% der Empfehlungen von IBCF entspricht. Für den ersten Datensatz lässt sich also eine stetige Zunahme der Übereinstimmungen und relativer Anteil der Übereinstimmungen feststellen. Eine ähnliche Zunahme lässt sich auch bei dem zweiten Datensatz feststellen. Allerdings sind die Anzahl Übereinstimmungen tiefer und von 40 zu 50 Singulärwerten werden keine zusätzlichen Übereinstimmungen mehr generiert.

Bei der Singulärwertezerlegung und -rekonstruktion werden die Resultate mit steigender Anzahl Singulärwerte zuerst raschen, dann langsamer besser. Ein ähnliches Verhalten sehen wir auch hier. Mit zunehmenden Singulärwerten wird die Übereinstimmung mit den Empfehlungen von IBCF höher. Das lässt nicht direkt den Schluss zu, dass IBCF besser als SVD mit einem tiefen Wert ist, aber die Resultate werden mit zunehmenden Singulärwerten besser.

8 Implementierung Top-N Metriken

Aufgabe 8 (DIY)

8.1 CoverageN

# Testing before creating the CoverageN function
# create ribcf Model
ribcf_8 <- Recommender(MovieLense, "IBCF", param=list(k= 30, method = "cosine"))
#ribcf_8

# predict all movies for every user
ribcftopNList_8 <- predict(ribcf_8, MovieLense, n=15)
#ribcftopNList_8

# get the list of unique items for all user
list_items_8 <- unique(unlist(as(ribcftopNList_8, "list"), use.names = FALSE))
#list_items_8

# get the length of this list
len_items_8 <- length(list_items_8)
paste("Top-N Liste der Kunden:", len_items_8)
[1] "Top-N Liste der Kunden: 694"
# get length of total items
len_all_items_8 <- dim(MovieLense)[2]
paste("Menge aller Produkte:", len_all_items_8)
[1] "Menge aller Produkte: 1664"
# calculate coverageN
coverageN <- len_items_8 / len_all_items_8
paste("coverageN fuer ribcf_8 model mit n = 15", round(coverageN, 4))
[1] "coverageN fuer ribcf_8 model mit n = 15 0.4171"
# create a function
coverageN <- function(model, n, dataset) {
  # predict all movies for every user
  topNList <- predict(model, dataset, n = n)
  
  # get the list of unique items for all users
  list_items <- unique(unlist(as(topNList, "list"), use.names = FALSE))
  
  # get the length of this list
  len_items <- length(list_items)
  
  # get length of total items
  len_all_items <- dim(dataset)[2]
  
  # calculate coverage
  coverage <- len_items / len_all_items
  
  return(coverage)
}

ribcf_8_coverage <- coverageN(ribcf_8, 15, MovieLense)
paste("Coverage fuer ribcf_8 model mit n = 15:", round(ribcf_8_coverage, 4))
[1] "Coverage fuer ribcf_8 model mit n = 15: 0.4171"
# List of n values
n_val <- c(5, 10, 15, 20, 25, 30)
n_dataset <- c(MovieLense, MovieLenseCompact_1, MovieLenseCompact_2)

# Create empty list
coverage_values <- c()

for (dataset in n_dataset) {
  # For loop to iterate over n_values
  ribcf_i <- Recommender(dataset, "IBCF", param=list(k= 30, method = "cosine"))
  print(dataset)
  
  for (n in n_val) {
    # calculate coverageN with ribcf_8
    coverage <- coverageN(ribcf_i, n, dataset)
    coverage_values <- c(coverage_values, coverage)
    print(paste("Coverage for n =", n,  round(coverage, 4)))
  }
}
943 x 1664 rating matrix of class ‘realRatingMatrix’ with 99392 ratings.
[1] "Coverage for n = 5 0.2344"
[1] "Coverage for n = 10 0.3341"
[1] "Coverage for n = 15 0.4171"
[1] "Coverage for n = 20 0.4874"
[1] "Coverage for n = 25 0.5379"
[1] "Coverage for n = 30 0.5823"
400 x 700 rating matrix of class ‘realRatingMatrix’ with 67427 ratings.
[1] "Coverage for n = 5 0.7171"
[1] "Coverage for n = 10 0.9014"
[1] "Coverage for n = 15 0.9629"
[1] "Coverage for n = 20 0.9743"
[1] "Coverage for n = 25 0.9829"
[1] "Coverage for n = 30 0.9886"
400 x 700 rating matrix of class ‘realRatingMatrix’ with 17784 ratings.
[1] "Coverage for n = 5 0.56"
[1] "Coverage for n = 10 0.7986"
[1] "Coverage for n = 15 0.9014"
[1] "Coverage for n = 20 0.9686"
[1] "Coverage for n = 25 0.9914"
[1] "Coverage for n = 30 0.9971"

Space Beitrag über Coverage: https://spaces.technik.fhnw.ch/spaces/recommender-systems/beitraege/recommender-system-evaluierung-coverage-und-novelty-1

Zur Überprüfung des Datensatzes haben wir die Anzahl Filme des aktuellen MovieLense Datensatzes ausgegeben, er beträgt weiterhin 1’664 Items. Unsere Berechnung anhand der Formel, die du im Space unter Beiträge gepostet hast, ergibt, dass 41.7 % der vorhandenen Filme empfohlen werden. Dies bestätigt unsere Erkenntnis aus Aufgabe 5, wonach nicht allen Usern die gleichen Items empfohlen werden. Auch erkennen wir, das durch die steigende Anzahl an n (Anzahl Items Empfehlung) erhöht sich auch entsprechend Coverage, was auch Sinn macht denn der Zähler, sprich die Anzahl einzigartige Filmen grösser sind bei einer grösseren Anzahl n, da mehr Filme für jeden User vorgeschlagen werden. Diese Beobachung können wir beim kompletten MovieLense Datensatz sowie bei den beiden reduzierten Datensätzen sehen.

8.2 NoveltyN

nratings(MovieLense) / len_all_items_8
[1] 59.73077
# Kreuztabelle erstellen Item vs Anzahl Rating
movie_ratings_counts <- table(MovieLenseEDA$item)

# dividieren durch Gesamtanzahl Item im Datensatz und logarithmieren
log_popularity <- log(movie_ratings_counts/dim(MovieLenseEDA)[2])

# Von jedem Film die Popularity als Dataframe
log_pop_df <- data.frame(log_popularity = log_popularity)
log_pop_df

# Anzahl User
S_user <- dim(MovieLense)[1]
paste("Anzahl aller Kunden", S_user)
[1] "Anzahl aller Kunden 943"
# Anzahl Items
N_item <- dim(MovieLense)[2]
paste("Anzahl aller Items", N_item)
[1] "Anzahl aller Items 1664"
# Model
ribcf_model <- Recommender(MovieLense, "IBCF", param=list(k= 30, method = "cosine"))

# get prediction for every user
ribcf_model_prediction <- predict(ribcf_8, MovieLense, n=15)

ribcf_model_prediction_list <- unlist(as(ribcf_model_prediction, "list"), use.names = FALSE)

# create a dataframe
ribcf_model_prediction_df <- data.frame(ribcf_model_prediction_list)

# join ribcf_model_prediction_df and log_pop_df
join_df <- merge(ribcf_model_prediction_df, log_pop_df, by.x = "ribcf_model_prediction_list", by.y = "log_popularity.Var1")

novelty <- -1/N_item * sum(join_df$log_popularity.Freq) / S_user

novelty
[1] 0.0001956961
# create function
novelty <- function(dataset, model, n) {
  
  movie_ratings_counts <- table(dataset$item)
  
  log_popularity <- log(movie_ratings_counts/dim(dataset)[2])
  
  log_pop_df <- data.frame(log_popularity = log_popularity)
  
  S_user <- dim(dataset)[1]
  N_item <- dim(dataset)[2]
  
  ribcf_model_prediction <- predict(model, dataset, n)
  
  ribcf_model_prediction_list <- unlist(as(ribcf_model_prediction, "list"), use.names = FALSE)
  
  ribcf_model_prediction_df <- data.frame(ribcf_model_prediction_list)
  
  join_df <- merge(ribcf_model_prediction_df, log_pop_df, by.x = "ribcf_model_prediction_list", by.y = "log_popularity.Var1")
  
  novelty <- -1/N_item * sum(join_df$log_popularity.Freq) / S_user
  
  return(novelty)
}


ribcf_model <- Recommender(MovieLense, "IBCF", param=list(k= 30, method = "cosine"))

#novelty(MovieLenseEDA, ribcf_model, 15)
# funktioniert nicht

Wieder in Anlehnung an deine Beschreibung dividieren wir die Anzahl sämtlicher Ratings durch die Anzahl Items. Das Resultat sagt aus, dass rund 60 mal mehr Ratings abgegeben wurden, als Items vorhanden sind.

to-do: Stimmt Berechnung von 8.2? Unterschiedliche Listenlängen!

9 Wahl des optimalen Recommenders

Aufgabe 9 ## 9.1 Verwende für die Evaluierung 10-fache Kreuzvalidierung

set.seed(1234)
scheme_1 <- evaluationScheme(MovieLenseCompact_1, method="cross-validation", k = 10, given=3, goodRating=5)
scheme_2 <- evaluationScheme(MovieLenseCompact_2, method="cross-validation", k = 10, given=3, goodRating=5)

print("Erste Datenreduktion")
[1] "Erste Datenreduktion"
scheme_1
Evaluation scheme with 3 items given
Method: ‘cross-validation’ with 10 run(s).
Good ratings: >=5.000000
Data set: 400 x 700 rating matrix of class ‘realRatingMatrix’ with 67427 ratings.
print("Zweite Datenreduktion")
[1] "Zweite Datenreduktion"
scheme_2
Evaluation scheme with 3 items given
Method: ‘cross-validation’ with 10 run(s).
Good ratings: >=5.000000
Data set: 400 x 700 rating matrix of class ‘realRatingMatrix’ with 17784 ratings.

9.2 Begründe deine Wahl von Metriken und Modell

algorithms <- list("hybrid" = list(name = "HYBRID", param =list(recommenders = list(SVD = list(name="SVD", param=list(k = 40)),
                                                                                    POPULAR = list(name = "POPULAR", param = NULL)
                                                                                    ))),
                   "libmf" = list(name="LIBMF", param=list(dim=10)),
                   "popular items" = list(name="POPULAR", param=NULL),
                   "user-based CF" = list(name="UBCF", param=list(nn=50)),
                   "item-based CF" = list(name="IBCF", param=list(k=50)),
                   "SVD40" = list(name="SVD", param=list(k = 40)))

print("Erster Datensatz")
[1] "Erster Datensatz"
results_1 <- evaluate(scheme_1, algorithms, type = "topNList", n=c(10, 15, 20, 25, 30))
HYBRID run fold/sample [model time/prediction time]
     1  [0.093sec/0.044sec] 
     2  [0.098sec/0.049sec] 
     3  [0.095sec/0.042sec] 
     4  [0.094sec/0.049sec] 
     5  [0.092sec/0.042sec] 
     6  [0.098sec/0.049sec] 
     7  [0.101sec/0.043sec] 
     8  [0.102sec/0.049sec] 
     9  [0.112sec/0.044sec] 
     10  [0.1sec/0.054sec] 
LIBMF run fold/sample [model time/prediction time]
     1  [0.001sec/0.077sec] 
     2  [0sec/0.077sec] 
     3  [0sec/0.075sec] 
     4  [0sec/0.073sec] 
     5  [0.001sec/0.072sec] 
     6  [0sec/0.071sec] 
     7  [0sec/0.071sec] 
     8  [0sec/0.074sec] 
     9  [0sec/0.073sec] 
     10  [0.001sec/0.072sec] 
POPULAR run fold/sample [model time/prediction time]
     1  [0.003sec/0.015sec] 
     2  [0.002sec/0.021sec] 
     3  [0.002sec/0.016sec] 
     4  [0.003sec/0.015sec] 
     5  [0.003sec/0.014sec] 
     6  [0.002sec/0.014sec] 
     7  [0.002sec/0.015sec] 
     8  [0.002sec/0.013sec] 
     9  [0.002sec/0.014sec] 
     10  [0.002sec/0.014sec] 
UBCF run fold/sample [model time/prediction time]
     1  [0.002sec/0.066sec] 
     2  [0.001sec/0.062sec] 
     3  [0.002sec/0.067sec] 
     4  [0.002sec/0.065sec] 
     5  [0.002sec/0.067sec] 
     6  [0.002sec/0.061sec] 
     7  [0.001sec/0.064sec] 
     8  [0.001sec/0.066sec] 
     9  [0.001sec/0.065sec] 
     10  [0.002sec/0.066sec] 
IBCF run fold/sample [model time/prediction time]
     1  [0.327sec/0.003sec] 
     2  [0.308sec/0.004sec] 
     3  [0.318sec/0.004sec] 
     4  [0.317sec/0.004sec] 
     5  [0.323sec/0.004sec] 
     6  [0.318sec/0.004sec] 
     7  [0.31sec/0.004sec] 
     8  [0.319sec/0.003sec] 
     9  [0.325sec/0.004sec] 
     10  [0.312sec/0.004sec] 
SVD run fold/sample [model time/prediction time]
     1  [0.09sec/0.017sec] 
     2  [0.094sec/0.008sec] 
     3  [0.096sec/0.008sec] 
     4  [0.102sec/0.008sec] 
     5  [0.103sec/0.008sec] 
     6  [0.096sec/0.008sec] 
     7  [0.101sec/0.007sec] 
     8  [0.102sec/0.008sec] 
     9  [0.104sec/0.008sec] 
     10  [0.092sec/0.007sec] 
print("Zweiter Datensatz")
[1] "Zweiter Datensatz"
results_2 <- evaluate(scheme_2, algorithms, type = "topNList", n=c(10, 15, 20, 25, 30))
HYBRID run fold/sample [model time/prediction time]
     1  [0.091sec/0.054sec] 
     2  [0.093sec/0.045sec] 
     3  [0.089sec/0.051sec] 
     4  [0.095sec/0.043sec] 
     5  [0.099sec/0.053sec] 
     6  [0.095sec/0.044sec] 
     7  [0.091sec/0.051sec] 
     8  [0.096sec/0.044sec] 
     9  [0.091sec/0.049sec] 
     10  [0.094sec/0.043sec] 
LIBMF run fold/sample [model time/prediction time]
     1  [0sec/0.032sec] 
     2  [0sec/0.032sec] 
     3  [0.001sec/0.033sec] 
     4  [0sec/0.033sec] 
     5  [0.001sec/0.032sec] 
     6  [0.001sec/0.035sec] 
     7  [0sec/0.035sec] 
     8  [0sec/0.035sec] 
     9  [0sec/0.033sec] 
     10  [0.001sec/0.033sec] 
POPULAR run fold/sample [model time/prediction time]
     1  [0.002sec/0.014sec] 
     2  [0.008sec/0.014sec] 
     3  [0.001sec/0.014sec] 
     4  [0.001sec/0.014sec] 
     5  [0.002sec/0.014sec] 
     6  [0.001sec/0.015sec] 
     7  [0.002sec/0.014sec] 
     8  [0.001sec/0.014sec] 
     9  [0.002sec/0.022sec] 
     10  [0.001sec/0.014sec] 
UBCF run fold/sample [model time/prediction time]
     1  
Timing stopped at: 0.013 0.001 0.014
Error in h(simpleError(msg, call)) : 
  error in evaluating the argument 'x' in selecting a method for function 't': not-yet-implemented method for <dgCMatrix> %*% <list>
IBCF run fold/sample [model time/prediction time]
     1  [0.159sec/0.004sec] 
     2  [0.161sec/0.004sec] 
     3  [0.159sec/0.006sec] 
     4  [0.158sec/0.005sec] 
     5  [0.157sec/0.025sec] 
     6  [0.152sec/0.004sec] 
     7  [0.162sec/0.004sec] 
     8  [0.153sec/0.003sec] 
     9  [0.159sec/0.004sec] 
     10  [0.152sec/0.004sec] 
SVD run fold/sample [model time/prediction time]
     1  [0.095sec/0.015sec] 
     2  [0.093sec/0.009sec] 
     3  [0.087sec/0.007sec] 
     4  [0.095sec/0.007sec] 
     5  [0.092sec/0.014sec] 
     6  [0.096sec/0.008sec] 
     7  [0.089sec/0.008sec] 
     8  [0.091sec/0.008sec] 
     9  [0.228sec/0.009sec] 
     10  [0.091sec/0.007sec] 
Warning: 
  Recommender 'user-based CF' has failed and has been removed from the results!

Dieser Print sagt nur aus, wie lange einzelne Berechnungen gedauert haben. Wir kommentieren ihn deshalb nicht vertieft.

plot(results_1, annotate=c(1,3), legend="topleft")

plot(results_2, annotate=c(1,3), legend="topleft")

9.3 Analysiere das beste Modell für Top-N Recommendations mit N = 10, 15, 20, 25 und 30,

In Aufgabe 9.2 haben wir eine ROC Kurve gemäss Vorlage von Recommenderlab erstellt. Auf der x-Achse befindet sich die FPR (false positive rate) und auf der y-Achse die TPR (true positive rate). Die Methode popular items generiert den tiefsten FPR und TPR für N = 10. Da die selbe Methode bis N = 30 das beste Verhältnis (höchste Kurve und höchster AUC) aufweist, ist dies das beste Modell. Den zweiten Platz teilen sich das hybride Modell und SVD mit 40 Werten. Die Kurve der beiden liegt ständig übereinander. Am schlechtesten hat das item-based CF Model abgeschnitten, welches zwar gleich wie das user-based angefangen hat, dann aber die TPR nicht mehr gleich stark verbessern konnte.

Beim zweiten Datensatz haben die Modell das gleiche Resultat generiert, ausser, dass die mittleren Modell näher zusammen liegen und item-based stärker zurück liegt. Für diesen Datensatz konnte das Modell von Recommenderlab kein Resultat für user-based berechnen. Die entsprechende Fehlermeldung ist auch bei der Generierung in Aufgabe 9.2 aufgetaucht.

Nachdem, was wir über Recommender Systems gelernt haben, haben wir eigentlich nicht erwartet, dass das popular Modell am besten abschneidet. Wir haben eher erwartet, dass du uns einen Datensatz gibst, bei dem user- und item-based besser abschneiden, weil wir im Lernmaterial viel darüber gelernt haben. Da das Resultat genau umgekehrt ist, haben wir überprüft, ob ein Fehler vorliegen könnte, wir haben diesen aber nicht gefunden.

Beim Vergleich der beiden Datensätze gehen wir davon aus, dass die tiefer TPR und leicht höhere FPR davon kommt, dass die Matrix erkennbar sparser ist und damit weniger Trainingsdaten vorhanden sind.

9.4 Optimiere dein bestes Modell hinsichtlich Hyperparametern

algorithmsimprovedrecom <- list("popular items center" = list(name="POPULAR", param=NULL),
                   "popular items Z-score" = list(name="POPULAR", param=list(normalize="Z-score")))

resultsimprovedrecom_1 <- evaluate(scheme_1, algorithmsimprovedrecom, type = "topNList", n=c(10, 15, 20, 25, 30))
POPULAR run fold/sample [model time/prediction time]
     1  [0.002sec/0.015sec] 
     2  [0.002sec/0.015sec] 
     3  [0.003sec/0.014sec] 
     4  [0.002sec/0.014sec] 
     5  [0.004sec/0.013sec] 
     6  [0.002sec/0.014sec] 
     7  [0.003sec/0.013sec] 
     8  [0.003sec/0.013sec] 
     9  [0.002sec/0.014sec] 
     10  [0.003sec/0.017sec] 
POPULAR run fold/sample [model time/prediction time]
     1  [0.006sec/0.026sec] 
     2  [0.005sec/0.015sec] 
     3  [0.005sec/0.015sec] 
     4  [0.006sec/0.014sec] 
     5  [0.005sec/0.015sec] 
     6  [0.005sec/0.015sec] 
     7  [0.006sec/0.023sec] 
     8  [0.005sec/0.014sec] 
     9  [0.005sec/0.015sec] 
     10  [0.004sec/0.015sec] 
resultsimprovedrecom_2 <- evaluate(scheme_2, algorithmsimprovedrecom, type = "topNList", n=c(10, 15, 20, 25, 30))
POPULAR run fold/sample [model time/prediction time]
     1  [0.001sec/0.014sec] 
     2  [0.003sec/0.013sec] 
     3  [0.001sec/0.02sec] 
     4  [0.001sec/0.014sec] 
     5  [0.001sec/0.013sec] 
     6  [0.002sec/0.013sec] 
     7  [0.001sec/0.015sec] 
     8  [0.001sec/0.014sec] 
     9  [0.001sec/0.014sec] 
     10  [0.001sec/0.015sec] 
POPULAR run fold/sample [model time/prediction time]
     1  [0.003sec/0.014sec] 
     2  [0.003sec/0.014sec] 
     3  [0.005sec/0.014sec] 
     4  [0.005sec/0.014sec] 
     5  [0.005sec/0.014sec] 
     6  [0.004sec/0.015sec] 
     7  [0.003sec/0.014sec] 
     8  [0.003sec/0.015sec] 
     9  [0.003sec/0.015sec] 
     10  [0.004sec/0.014sec] 
plot(resultsimprovedrecom_1, annotate=c(1,3), legend="topleft")

plot(resultsimprovedrecom_2, annotate=c(1,3), legend="topleft")

Nachdem wir festgestellt haben, dass popular das beste Modell ist, blieb uns für die Optimierung der Hyperparameter nur die Anpassung der Normierung. Einerseits steht die klassische Zentralisierung der Daten und andererseits die Normierung mittels Z-Score zur Verfügung. Das Modell hat mit den selben N Werten keine sichtbaren Unterschiede zwischen den Normierungsmethoden generiert. Das gilt für beide Datensätze. Da die dargestellte Berechnungsdauer für beide Methoden gleich lang ist, entscheiden wir uns für die normale Normierung, da diese einfacher zu berechnen ist. Wir erwarten dadurch einen Performancevorteil bei grösseren Datensätzen.

10 Implementierung Top-N Monitor

Aufgabe 10 (DIY): Untersuche die relative Übereinstimmung zwischen Top-N Empfehlungen und präferierten Filmen für 4 unterschiedliche Modelle (z.B. IBCF und UBCF mit unterschiedlichen Ähnlichkeitsmetriken / Nachbarschaften sowie SVD mit unterschiedlicher Dimensionalitätsreduktion).

10.1 Fixiere 20 zufällig gewählte Testkunden für alle Modellvergleiche,

# Testing before creating function
# select 20 random users
set.seed(1234)
testUsers <- sample(1:nrow(MovieLense), 20)
testUsers
 [1] 284 848 918 101 623 905 645 934 400 900  98 103 726 602 326  79 884 270 382 184
# filter MovieLense by testUsers
MovieLenseTest <- MovieLense[testUsers,] 
MovieLenseTest
20 x 1664 rating matrix of class ‘realRatingMatrix’ with 1641 ratings.

In dieser Zelle haben wir die Auswahl der zufälligen Kunden getestet, bevor wir eine Funktion erstellen.

# create function for select random users
select_random_users <- function(data, num_users, seed) {
  set.seed(seed)
  testUsers <- sample(1:nrow(data), num_users)
  dataTest <- data[testUsers,]
  
  return(list(testUsers, dataTest))
}

select_random_user <- select_random_users(MovieLense, 20, 1234)

testUsers <- select_random_user[[1]]
MovieLenseTest <- select_random_user[[2]]
testUsers
 [1] 284 848 918 101 623 905 645 934 400 900  98 103 726 602 326  79 884 270 382 184
MovieLenseTest
20 x 1664 rating matrix of class ‘realRatingMatrix’ with 1641 ratings.

Nun haben wir die Funktion erstellt und sie auf dem gleichen Datensatz getestet. Das Resultat ist wieder das gleiche.

# Make for both datasets
# dataset 1
select_random_user_1 <- select_random_users(MovieLenseCompact_1, 20, 1234)

testUsers_1 <- select_random_user_1[[1]]
MovieLenseTest_1 <- select_random_user_1[[2]]
testUsers_1
 [1] 284 336 101 111 393 133 388  98 103 214  90 326  79 372 270 382 184  62   4 149
MovieLenseTest_1
20 x 700 rating matrix of class ‘realRatingMatrix’ with 3234 ratings.
# dataset 2
select_random_user_2 <- select_random_users(MovieLenseCompact_2, 20, 123)

testUsers_2 <- select_random_user_2[[1]]
MovieLenseTest_2 <- select_random_user_2[[2]]
testUsers_2
 [1] 179  14 195 306 118 299 229 244 399 374 153  90  91 256 197 388 348 137 355 328
MovieLenseTest_2
20 x 700 rating matrix of class ‘realRatingMatrix’ with 716 ratings.

Wir haben nun die Funtkion auf beide reduzierten Datensätze angewendet. Da der selbe Seed trotzdem die gleichen User ausgewählt hat, mussten wir ihn unterschiedlich definieren.

10.2 Bestimme den Anteil der Top-N Empfehlung nach Genres pro Kunde,

# Modelle erstellen
ribcf_10 <- Recommender(MovieLenseTest, "IBCF", param=list(k= 30, method = "cosine"))

rubcf_10 <- Recommender(MovieLenseTest, "UBCF", parameter=list(method = "Euclidean", nn = 10, normalize = "Z-score"))

rsvd_3 <- Recommender(MovieLenseTest, "SVD", param = list(k = 3))

rsvd_5 <- Recommender(MovieLenseTest, "SVD", param = list(k = 5))

Hier haben wir die vier Modelle trainiert.

#ribcf_10
# predict Top-N items for every user
ribcftopNList_10 <- predict(ribcf_10, MovieLenseTest, n=15)

# create a list with the topN items for every user
ribcftopNList_10_list <- as(ribcftopNList_10, "list")

# create a tibble with the topN items for every user
ribcftopNList_10_tibble <- as_tibble(ribcftopNList_10_list)

# transform the tibble to a data frame
ribcftopNList_10_df <- as.data.frame(ribcftopNList_10_tibble)

# replace colname with testUsers
colnames(ribcftopNList_10_df) <- testUsers

# transpose data frame
ribcftopNList_10_df_transposed <- t(ribcftopNList_10_df)

# change ribcftopNList_10_df_transposed to a tibble
ribcftopNList_10_df_transposed_tibble <- as_tibble(ribcftopNList_10_df_transposed)

# add a column with the testUsers
ribcftopNList_10_df_transposed_tibble$testUsers <- testUsers

# pivot longer dataframe
ribcftopNList_10_df_transposed_tibble_pivot <- pivot_longer(ribcftopNList_10_df_transposed_tibble, cols = 1:15, names_to = "topN", values_to = "itemID")

# get genre from each item
ribcftopNList_10_df_transposed_tibble_pivot_genre <- left_join(ribcftopNList_10_df_transposed_tibble_pivot, MovieLenseMeta, by = c("itemID" = "title"))
ribcftopNList_10_df_transposed_tibble_pivot_genre

# drop columns topN, year, url
ribcftopNList_10_df_transposed_tibble_pivot_genre <- select(ribcftopNList_10_df_transposed_tibble_pivot_genre, -topN, -year, -url, -itemID)
ribcftopNList_10_df_transposed_tibble_pivot_genre

Diese und die nächste Zelle stellt unser Testing der Auswertung dar.

# pivot longer dataframe
topnmonitor_recom <- ribcftopNList_10_df_transposed_tibble_pivot_genre %>% group_by(testUsers) %>%
  summarise(across(everything(), ~ sum(., is.na(.), 0)))
topnmonitor_recom
get_top_n_items <- function(model, dataset, userlist) {
  # predict Top-N items for every user
  top_n_list <- predict(model, dataset, n = 15)
  print(top_n_list)
  
  # create a list with the topN items for every user
  top_n_list_list <- as(top_n_list, "list")
  
  # create a tibble with the topN items for every user
  top_n_list_tibble <- as_tibble(top_n_list_list)
  
  # transform the tibble to a data frame
  top_n_list_df <- as.data.frame(top_n_list_tibble)
  
  # replace colname with testUsers
  colnames(top_n_list_df) <- userlist
  
  # transpose data frame
  top_n_list_df_transposed <- t(top_n_list_df)
  
  # change top_n_list_df_transposed to a tibble
  top_n_list_df_transposed_tibble <- as_tibble(top_n_list_df_transposed)
  
  # add a column with the testUsers
  top_n_list_df_transposed_tibble$testUsers <- userlist
  
  # pivot longer dataframe
  top_n_list_df_transposed_tibble_pivot <- pivot_longer(top_n_list_df_transposed_tibble, cols = 1:15, names_to = "topN", values_to = "itemID")
  
  # get genre from each item
  top_n_list_df_transposed_tibble_pivot_genre <- left_join(top_n_list_df_transposed_tibble_pivot, MovieLenseMeta, by = c("itemID" = "title"))
  
  # drop columns topN, year, url
  top_n_list_df_transposed_tibble_pivot_genre <- select(top_n_list_df_transposed_tibble_pivot_genre, -topN, -year, -url, -itemID)
  
  return(top_n_list_df_transposed_tibble_pivot_genre)
}

ribcftopNList_10_df_transposed_tibble_pivot_genre <- get_top_n_items(ribcf_10, MovieLenseTest, testUsers)
Recommendations as ‘topNList’ with n = 15 for 20 users. 
ribcftopNList_10_df_transposed_tibble_pivot_genre

Diese Zelle war ein Testing der neu geschriebenen Funktion.

# both datasets
n_dataset <- list(MovieLenseCompact_1, MovieLenseCompact_2)

cnt <- 1
top_n_list <- list()


for (i in 1:length(n_dataset)) {
  select_random_user <- select_random_users(n_dataset[[i]], 20, 1234)
  testUser_i <- select_random_user[[1]]
  MovieLense_i <- select_random_user[[2]]
  
  ribcf_10 <- Recommender(n_dataset[[i]], "IBCF", param=list(k= 30, method = "cosine"))

  rubcf_10 <- Recommender(n_dataset[[i]], "UBCF", parameter=list(method = "Euclidean", nn = 10, normalize = "Z-score"))

  rsvd_3 <- Recommender(n_dataset[[i]], "SVD", param = list(k = 3))

  rsvd_5 <- Recommender(n_dataset[[i]], "SVD", param = list(k = 5))
  
  n_models <- list(ribcf_10, rubcf_10, rsvd_3, rsvd_5)
  print(n_dataset[i])
  for (j in 1:length(n_models)) {
    get_top_n_items_df <- get_top_n_items(n_models[[j]], MovieLense_i, testUser_i)
    topnmonitor_recom <- get_top_n_items_df %>% group_by(testUsers) %>%
    summarise(across(everything(), ~ sum(., is.na(.), 0)))
    
    top_n_list[[cnt]] <- topnmonitor_recom
    cnt <- cnt + 1
  }
}  
[[1]]
400 x 700 rating matrix of class ‘realRatingMatrix’ with 67427 ratings.

Recommendations as ‘topNList’ with n = 15 for 20 users. 
Recommendations as ‘topNList’ with n = 15 for 20 users. 
Recommendations as ‘topNList’ with n = 15 for 20 users. 
Recommendations as ‘topNList’ with n = 15 for 20 users. 
[[1]]
400 x 700 rating matrix of class ‘realRatingMatrix’ with 17784 ratings.

Recommendations as ‘topNList’ with n = 15 for 20 users. 
Recommendations as ‘topNList’ with n = 15 for 20 users. 
Recommendations as ‘topNList’ with n = 15 for 20 users. 
Recommendations as ‘topNList’ with n = 15 for 20 users. 
  

top_n_list
[[1]]

[[2]]

[[3]]

[[4]]

[[5]]

[[6]]

[[7]]

[[8]]
NA

Diese Auswertung stellt nun die Verteilung der top empfohlenen Genre fürs die einzelnen Kunden dar. Im ersten Dataframe wird ersichtlich, dass für User 4 viermal Action Filme empfohlen wurden. Drama Filme befanden sich gar 10 mal unter den Filmen.

10.3 Bestimme pro Kunde den Anteil nach Genres seiner Top-Filme

(=Filme mit besten Bewertungen),

topnmonitor_fav_movies <- MovieLenseEDA_Joined %>% filter(user %in% testUsers, rating == 5) %>% group_by(user) %>% summarise(across(c(unknown, Action, Adventure, Animation, `Children's`, Comedy, Crime, Documentary, Drama, Fantasy, `Film-Noir`, Horror, Musical, Mystery, Romance, `Sci-Fi`, Thriller, War, Western),sum)) %>% mutate(user = as.numeric(user)) %>%  arrange(user)
topnmonitor_fav_movies

Hierbei handelt es sich wieder um ein Resultat eines Tests.

# create function
get_fav_movies_by_genre <- function(users) {
  fav_movies_by_genre <- MovieLenseEDA_Joined %>% filter(user %in% users, rating == 5) %>% group_by(user) %>% summarise(across(c(unknown, Action, Adventure, Animation, `Children's`, Comedy, Crime, Documentary, Drama, Fantasy, `Film-Noir`, Horror, Musical, Mystery, Romance, `Sci-Fi`, Thriller, War, Western),sum)) %>% mutate(user = as.numeric(user)) %>%  arrange(user)
  return(fav_movies_by_genre)
}

topnmonitor_fav_movies <- get_fav_movies_by_genre(testUsers)
topnmonitor_fav_movies

Bei diesem Test der Funktion wurde wieder dasselbe Resultat generiert.

# iterate throguh both test_users_i
topnmonitor_fav_movies_1 <- get_fav_movies_by_genre(testUsers_1)
topnmonitor_fav_movies_1

topnmonitor_fav_movies_2 <- get_fav_movies_by_genre(testUsers_2)
topnmonitor_fav_movies_2

topnmonitor_fav_movies_list <- list(topnmonitor_fav_movies_1, topnmonitor_fav_movies_2)

Diese Funktion hat nun die Top Genres der von den Usern bewerteten Filmen generiert. Im zweiten Dataframe ist ersichtlich, dass sieben von User 4 “gelikte” Filme vom Genre Action kommen.

10.4 Vergleiche pro Kunde Top-Empfehlungen vs Top-Filme nach Genres,

a <- topnmonitor_recom[1,2:20]
b <- topnmonitor_fav_movies[1,2:20]

binded <- rbind(a, b)
binded_complete <- binded %>% add_column(Type = c("topnmonitor_recom", "topnmonitor_fav_movies"))
binded_complete

# pivot_longer binded_complete dataframe
binded_complete_pivot <- pivot_longer(binded_complete, cols = 1:19, names_to = "genre", values_to = "value")
binded_complete_pivot

# create cleveland plot
ggplot(binded_complete_pivot, aes(y = genre, x = value)) +
  geom_point(aes(color = Type)) + 
  geom_line(aes(group=genre)) + 
  theme_minimal() +
  labs(title = "Top-N Empfehlungen vs Top-Filme nach Genres", x = "Value", y = "Genre") +
  theme(plot.title = element_text(hjust = 0.5))

Hierbei handelt es sich wieder um einen Test für den Cleveland Plot.

create_cleveland_plot <- function(a, b, datatext, modeltext){
  binded <- rbind(a, b)
  binded_complete <- binded %>% add_column(Type = c("topnmonitor_recom", "topnmonitor_fav_movies"))
  binded_complete_pivot <- pivot_longer(binded_complete, cols = 1:19, names_to = "genre", values_to = "value")
  ggplot(binded_complete_pivot, aes(y = genre, x = value)) +
    geom_point(aes(color = Type)) + 
    geom_line(aes(group=genre)) + 
    theme_minimal() +
    labs(title = paste("Top-N Empfehlungen vs Top-Filme nach Genres mit Modell", modeltext, "für", datatext) , x = "Value", y = "Genre") +
    theme(plot.title = element_text(hjust = 0.5))
}

Diese Funktion generiert die Cleveland Plots.

#models und datensatz finden und zum titel hinzfuegen.
n_models <- list("IBCF", "UBCF", "SVD_3", "SVD_5")
n_dataset <- list("Datensatz 1", "Datensatz 2")

cnt <- 1
for (b_i in 1:length(n_dataset)) {
  datatext_i <- n_dataset[b_i]
  for (a_i in 1:length(n_models)) {
    modeltext_i <- n_models[a_i]
    b_df <- topnmonitor_fav_movies_list[[b_i]][1,2:20]
    a_df <- top_n_list[[cnt]][1,2:20]
    cnt <- cnt + 1
    print(create_cleveland_plot(a_df, b_df, datatext_i, modeltext_i))
  }
}

Es wurden nun 8 Cleveland Plots generiert. Vier für den ersten reduzierten Datensatz und vier für den zweiten reduzierten Datensatz. Die vier Plots pro Datensatz stehen für je eines der unterschiedlichen Modelle. Dargestellt wird jeweils das Resultat des ersten Users pro Datensatz. Im ersten Plot (IBCF für Datensatz 1) ist ersichtlich, dass dem User 10mal Drama Filme empfohlen hat, während er selber sie nur 3mal gelikt hat. Der schwarze Balken entspricht der Differenz.

Der visuelle Vergleich zwischen den beiden Datensätzen lässt den Schluss zu, dass bei beiden grosse Diskrepanzen zwischen den Empfehlungen und den tatsächlichen Likes bestehen.

10.5 Definiere eine Qualitätsmetrik für Top-N Listen und teste sie.

# testing
rowSums(topnmonitor_recom[2:20] * topnmonitor_fav_movies[2:20]) /
(sqrt(rowSums(topnmonitor_recom[2:20]^2))*sqrt(rowSums(topnmonitor_fav_movies[2:20]^2)))
 [1] 0.9069238 0.5152888 0.6301260 0.7570982 0.8853280 0.8239333 0.8400269 0.7133149 0.8489614 0.6425396 0.3520915 0.5471347 0.9303025 0.5251318
[15] 0.7819307 0.9032992 0.0000000 0.3034885 0.8848772 0.8151546

Hier handelt es sich um wiederum um einen Test.

cosine_10_2df <- function(df1, df2) {
  cosine <- rowSums(df1[2:20] * df2[2:20]) /
  (sqrt(rowSums(df1[2:20]^2))*sqrt(rowSums(df2[2:20]^2)))
  return(cosine)
} 

cnt <- 1
for (b_i in 1:length(n_dataset)) {
  datatext_i <- n_dataset[b_i]
  for (a_i in 1:length(n_models)) {
    modeltext_i <- n_models[a_i]
    b_df <- topnmonitor_fav_movies_list[[b_i]]
    a_df <- top_n_list[[cnt]]
    cnt <- cnt + 1
    print(paste("Model:", modeltext_i, "fuer:", datatext_i))
    print(cosine_10_2df(a_df, b_df))
  }
}
[1] "Model: IBCF fuer: Datensatz 1"
 [1] 0.6935430 0.8455943 0.9021505 0.8883147 0.6367633 0.4698092 0.8401314 0.6912801 0.6005326 0.7186270 0.7365998 0.7239256 0.8541691 0.7004148
[15] 0.7998114 0.6647630 0.6591637 0.7677265 0.8826991 0.8451733
[1] "Model: UBCF fuer: Datensatz 1"
 [1] 0.8582787 0.7131432 0.7765745 0.9219525 0.5300564 0.8314972 0.6639960 0.3900120 0.5170877 0.7733603 0.7843665 0.9135536 0.7433708 0.7563788
[15] 0.8659243 0.6220060 0.4728500 0.9133966 0.7486150 0.8489526
[1] "Model: SVD_3 fuer: Datensatz 1"
 [1] 0.8027351 0.6950060 0.7682045 0.8002247 0.4414853 0.6504436 0.8117077 0.8314794 0.4163455 0.6956249 0.9018489 0.9436578 0.8389933 0.7619048
[15] 0.7436476 0.6797812 0.6419142 0.8550946 0.8008105 0.8300497
[1] "Model: SVD_5 fuer: Datensatz 1"
 [1] 0.8027351 0.7111516 0.9022537 0.8448912 0.3033761 0.7145896 0.7700535 0.8123624 0.4254570 0.6205052 0.8891479 0.9436578 0.8266660 0.7559289
[15] 0.7581599 0.6804352 0.6610103 0.8879845 0.8096001 0.8306839
[1] "Model: IBCF fuer: Datensatz 2"
 [1] 0.8678634 0.9050597 0.7011836 0.7616374 0.7817351 0.5529073 0.5668606 0.7481320 0.7588051 0.4413833 0.7144856 0.9348218 0.8342001 0.7432423
[15] 0.7911594 0.7642934 0.5416026 0.7576729 0.7210929 0.6929349
[1] "Model: UBCF fuer: Datensatz 2"
 [1] 0.8711082 0.9610996 0.7221875 0.8493924 0.5805404 0.6216373 0.5318160 0.7445007 0.5169973 0.7514691 0.8102991 0.8446167 0.7305575 0.6003573
[15] 0.9433823 0.7701016 0.6432675 0.6768635 0.7163354 0.7994862
[1] "Model: SVD_3 fuer: Datensatz 2"
 [1] 0.8281583 0.9379023 0.5643960 0.7717473 0.3305857 0.7681215 0.7852190 0.7839878 0.3909129 0.8731283 0.8900629 0.7697752 0.8423966 0.8075135
[15] 0.6931611 0.7098416 0.5373284 0.8077686 0.7994082 0.8344919
[1] "Model: SVD_5 fuer: Datensatz 2"
 [1] 0.8281583 0.9413758 0.5780489 0.7717473 0.3305857 0.7177652 0.7852190 0.7839878 0.3909129 0.8731283 0.8900629 0.7520047 0.8423966 0.8075135
[15] 0.6931611 0.7098416 0.6114296 0.8077686 0.7994082 0.8273288

Wir haben uns für die Qualitätsmetrik Cosine Similarity entschieden. Da wir pro User zwei Vektoren haben, ist dies eine geeignete Metrik, um die Top-N Liste und die Top-Filme des Kunden zu vergleichen. Für das Model IBCF und den Datensatz 1 wurde für den zweiten User eine Cosine Similarity von 0.85 berechnet. Die Übereinstimmung zwischen den Top-N empfohlenen Genres und den Top-Film-Genres des Kunden ist also relativ hoch. Im Gegensatz dazu ist beim sechsten User die Similarity lediglich 0.47 und die Übereinstimmung damit merkbar tiefer. Innerhalb des ersten Datensatzes sind die Similarities relativ hoch. Bei allen Modellen liegen die Werte grösstenteils über 0.7. Dies gilt auch für den zweiten Datensatz. Nach oben gehen die Werte bis fast 1 nach unten bis 0.4. Verglichen zwischen dem ersten und zweiten Datensatz, sind die Werte ebenfalls ähnlich. Wir haben zwar keine Auswertung berechnet, doch sind die Zahlen im gleichen, eher hohen, Bereich.

LS0tCnRpdGxlOiAiUlNZLU1DMSIKYXV0aG9yOiAiUGF0cmljayBTY2jDvHJtYW5uLCBTaSBCZW4gVHJhbiIKb3V0cHV0OiAKICBodG1sX25vdGVib29rOgogICAgdG9jOiBUcnVlCiAgICB0b2NfZmxvYXQ6IFRydWUKLS0tCiMgTWluaS1DaGFsbGVuZ2UgQmVzY2hyZWlidW5nCiMgRGF0ZW4gdW5kIExpYnJhcmllcwpgYGB7cn0KbGlicmFyeSh0aWR5dmVyc2UpCmxpYnJhcnkocmVjb21tZW5kZXJsYWIpCmxpYnJhcnkoZ3JpZEV4dHJhKQpgYGAKCiMgRGF0ZW4gZWlubGVzZW4KYGBge3J9CmRhdGEoTW92aWVMZW5zZSkKYGBgCgpCZWltIEVpbmxlc2VuIGRlcyBEYXRlbnNhdHplcyB3ZXJkZW4gZHJlaSByZWFsUmF0aW5nTWF0cml4IGVpbmdlbGVzZW46IE1vdmllTGVuc2UsIE1vdmllTGVuc2VNZXRhIHVuZCBNb3ZpZUxlbnNlVXNlci4gV2lyIHVudGVyc3VjaGVuIG51biB6dWVyc3QgTW92aWVMZW5zZS4KCiMgTGlicmFyeSBNZXRob2RlbiB6dW0gRGF0ZW5zYXR6IE1vdmllTGVuc2UKYGBge3J9Cm1ldGhvZHMoY2xhc3MgPSBjbGFzcyhNb3ZpZUxlbnNlKSkKYGBgCkRpZXNlIMOcYmVyc2ljaHQgemVpZ3QgdW5zLCB3ZWxjaGUgTWV0aG9kZW4gbWl0IGRlciByZWFsUmF0aW5nTWF0cml4IGluIEtvbWJpbmF0aW9uIG1pdCBSZWNvbW1lbmRlcmxhYiBtw7ZnbGljaCBzaW5kLgoKIyBNb3ZpZUxlbnNlIERhdGFmcmFtZSBlcnN0ZWxsZW4KYGBge3J9Ck1vdmllTGVuc2VFREEgPC0gYXMoTW92aWVMZW5zZSwgImRhdGEuZnJhbWUiKQpgYGAKClVtIGRlbiBFREEtVGVpbCBsw7ZzZW4genUga8O2bm5lbiwgaGFiZW4gd2lyIGRpZSByZWFsUmF0aW5nTWF0cml4IGluIGVpbmVuIGRhdGEuZnJhbWUgdW1nZXdhbmRlbHQuCgojIEtvcGZ6ZWlsZSB1bmQgRnVzc3plaWxlIHZvbSBEYXRlbnNhdHogYXVzZ2ViZW4KYGBge3J9CmhlYWQoTW92aWVMZW5zZUVEQSkKCnRhaWwoTW92aWVMZW5zZUVEQSkKYGBgCgpVbSBlaW5lIElkZWUgZGVyIERhdGVuIHp1IGVyaGFsdGVuLCBoYWJlbiB3aXIgZGVuIEhlYWQgdW5kIFRhaWwgZGVzIERhdGFmcmFtZXMgYXVzZ2VnZWJlbi4gRXMgd2lyZCBlcnNpY2h0bGljaCwgZGFzcyBmw7xyIGplZGUgWmVpbGUgZWluIFVzZXIsIEl0ZW0gKEZpbG0pIHVuZCBkYXMgUmF0aW5nIGVyZmFzc3Qgc2luZC4KCiMgSW5mb3MgenVtIERhdGVuc2F0egpgYGB7cn0Kc3VtbWFyeShNb3ZpZUxlbnNlRURBKQpgYGAKCk1pdCBkZXIgU3VtbWFyeSBGdW5rdGlvbiBoYWJlbiB3aXIgdW5zIGVpbmVuIMOcYmVyYmxpY2sgw7xiZXIgZGllIFphaGxlbiBpbSBEYXRlbnNhdHogZXJzY2hhZmZlbi4gRXMgc2luZCBqZXdlaWxzIDk5JzM5MiBVc2VyIHVuZCBJdGVtcyBlcmZhc3N0LiBEaWUgUmF0aW5ncyByZWljaGVuIHZvbSBCZXJlaWNoIDEgYmlzIDUgdW5kIGRlciBNaXR0ZWx3ZXJ0IGJldHLDpGd0IDMuNTMgKE1lZGlhbiA0LjApCgojIDEgRXhwbG9yYXRpdmUgRGF0ZW5hbmFseXNlCkF1ZmdhYmUgMTogVW50ZXJzdWNoZSBkZW4gdm9sbHN0w6RuZGlnZW4gTW92aWVMZW5zZSBEYXRlbnNhdHogCihkLmguIHZvciBEYXRlbnJlZHVrdGlvbiEpIHVuZCBiZWFudHdvcnRlIGZvbGdlbmRlIEZyYWdlbjoKCiMjIDEuMSBXZWxjaGVzIHNpbmQgZGllIGFtIGjDpHVmaWdzdGVuIGdlc2NoYXV0ZW4gR2VucmVzL0ZpbG1lPwoKIyMjIDEuMS4xIFdlbGNoZXMgc2luZCBkaWUgYW0gaMOkdWZpZ3N0ZW4gZ2VzY2hhdXRlbiBGaWxtZT8KYGBge3J9Ck1vdmllTGVuc2VFREEgJT4lIAogIGdyb3VwX2J5KGl0ZW0pICU+JSAKICBzdW1tYXJpc2UoQW56YWhsID0gbigpKSAlPiUKICBhcnJhbmdlKGRlc2MoQW56YWhsKSkgJT4lIAogIGhlYWQobj0xMCkKYGBgCkRpZXNlIFRhYmVsbGUgemVpZ3QgdW5zIGRpZSBUb3AtMTAgZGVyIGFtIG1laXN0ZW4gZ2VzY2hhdXRlbiwgcmVzcC4gZ2VyYXRldGVuIEZpbG1lLiBFcyB3aXJkIGVyc2ljaHRsaWNoLCBkYXNzIFN0YXIgV2FycyAoMTk3NywgdmVybXV0bGljaCAiS3JpZWcgZGVyIFN0ZXJuZSIpIDU4MyBtYWwgZ2VzY2hhdXQgdW5kIGdlcmF0ZXQgd3VyZGUuIAoKIyMjIDEuMS4yIFdlbGNoZXMgc2luZCBkaWUgYW0gaMOkdWZpZ3N0ZW4gZ2VzY2hhdXRlbiBHZW5yZT8KYGBge3J9CiMgRnVsbCBKb2luIG1pdCBkZl9tb3ZpZXNfcmF0aW5nIHVuZCBNb3ZpZUxlbnNlTWV0YQpNb3ZpZUxlbnNlRURBX0pvaW5lZCA8LSBmdWxsX2pvaW4oTW92aWVMZW5zZUVEQSwgTW92aWVMZW5zZU1ldGEsIAogICAgICAgICAgYnkgPSBjKCJpdGVtIiA9ICJ0aXRsZSIpKSAlPiUgCiAgc2VsZWN0KC1jKCJ1c2VyIiwgIml0ZW0iLCAicmF0aW5nIiwgInllYXIiLCAidXJsIikpIAoKIyBBdWZzdW1taWVyZW4gZGVyIEdlbnJlIFNwYWx0ZW4KKGNvbFN1bXMoTW92aWVMZW5zZUVEQV9Kb2luZWQpKSAlPiUgc29ydChkZWNyZWFzaW5nID0gVFJVRSkKCmBgYApXaXIgZXJrZW5uZW4sIGRhc3MgZGFzIGFtIGjDpHVmaWdzdGVuIGdlc2NoYXV0ZW4gR2VucmUgIkRyYW1hIiBtaXQgMzknNDQ2IFJhdGluZ3MgaXN0LiBBdWYgZGVtIHp3ZWl0ZW4gUGxhdHogYmVmaW5kZXQgc2ljaCAiQ29tZGV5IiB1bmQgYXVmIGRlbSBkcml0dGVuICJBY3Rpb24iLiBBbSB3ZW5pZ3N0ZW4gaMOkdWZpZyB3dXJkZW4gIkRvY3VtZW50YXJ5IiB1bmQgInVua25vd24iIGdlc2NoYXV0LiAKCgojIyAxLjIgV2llIHZlcnRlaWxlbiBzaWNoIGRpZSBLdW5kZW5yYXRpbmdzIGdlc2FtdGhhZnQgdW5kIG5hY2ggR2VucmVzPwpgYGB7cn0KIyBEYXRhRnJhbWUgam9pbgpNb3ZpZUxlbnNlRURBX0pvaW5lZCA8LSBmdWxsX2pvaW4oTW92aWVMZW5zZUVEQSwgTW92aWVMZW5zZU1ldGEsIAogICAgICAgICAgYnkgPSBjKCJpdGVtIiA9ICJ0aXRsZSIpKQpgYGAKCkbDvHIgZGllc2UgRnJhZ2UgaGFiZW4gd2lyIGVpbmVuIG5ldWVuIERhdGVuc2F0eiAiTW92aWVMZW5zZUVEQV9Kb2luZWQiIGVyc3RlbGx0LiBFciBlcmdpYnQgc2ljaCBhdXMgTW92aWVMZW5zZUVEQSB1bmQgTW92aWVMZW5zZU1ldGEuIEZvbGdlbmQgbnVuIGRpZSBCZWFudHdvcnR1bmQgZGVyIEZyYWdlLgoKIyMjIDEuMi4xIFZlcnRlaWx1bmcgZGVyIEt1bmRlbnJhdGluZ3MgR2VzYW10aGFmdApgYGB7cn0KTW92aWVMZW5zZUVEQV9Kb2luZWQkcmF0aW5nIDwtIGFzLmZhY3RvcihNb3ZpZUxlbnNlRURBX0pvaW5lZCRyYXRpbmcpCgojIERhdGFmcmFtZSBVZWJlcnNpY2h0Ck1vdmllTGVuc2VFREFfSm9pbmVkICU+JSBncm91cF9ieShyYXRpbmcpICU+JQogIHN1bW1hcml6ZShBbnphaGwgPSBuKCkpCgojIFZpc3VlbGxlIERhcnN0ZWxsdW5nIG1pdHRlbHMgQmFycGxvdApNb3ZpZUxlbnNlRURBX0pvaW5lZCAlPiUgZ3JvdXBfYnkocmF0aW5nKSAlPiUKICBzdW1tYXJpemUoQW56YWhsID0gbigpKSAlPiUgCiAgZ2dwbG90KGFlcyh4ID0gcmF0aW5nLCB5ID0gQW56YWhsKSkgKyAKICBnZW9tX2JhcihzdGF0ID0gImlkZW50aXR5IiwgCiAgICAgICAgICAgZmlsbCA9ICJsaWdodGJsdWUiLCAKICAgICAgICAgICBjb2xvciA9ICJibGFjayIpICsgCiAgbGFicyh4ID0gIlJhdGluZ3MiLCAKICAgICAgIHkgPSAiQW56YWhsIiwgCiAgICAgICB0aXRsZSA9ICJWZXJ0ZWlsdW5nIGRlciBLdW5kZW5yYXRpbmdzIEdlc2FtdGhhZnQiLAogICAgICAgc3VidGl0bGUgPSBwYXN0ZSgiR2VzYW10ZSBBbnphaGwgS3VuZGVucmF0aW5nczoiLCBkaW0oTW92aWVMZW5zZUVEQV9Kb2luZWQpWzFdKSkKYGBgCldpZSB3aXIgaW4gaW0gRGF0YWZyYW1lIHNvd2llIGltIEJhcnBsb3QgZXJrZW5uZW4sIHdlcmRlbiBhbSBow6R1Zmlnc3RlbiBkaWUgUmF0aW5ncyAzIHVuZCAgNCB2ZXJnZWJlbi4gUmF0aW5nIHZvbiAxIHVuZCAyIGtvbW1lbiBkZXV0bGljaCB3ZW5pZ2VyIHZvciwgYWxzIG3DtmdsaWNoZXIgR3J1bmQga8O2bm50ZSBzZWluLCBkYXNzIEZpbG1lIGRpZSBzY2hsZWNodCBzaW5kIGdhciBuaWNodCBiZXdlcnRldCB3dXJkZW4sIGRhIG1hbiBzaWNoIG5pY2h0IG1laHIgd2VpdGVyIG1pdCBzY2hsZWNodGVuIEZpbG1lbiBiZWZhc3NlbiBtw7ZjaHRlLiBBdXMgZWlnZW5lbiBFcmZhaHJ1bmdlbiBrw7ZubmVuIHdpciBzYWdlbiwgZGFzcyBtYW4gZWhlciBtZWhyIGJlcmVpdCBpc3QgZWluZW4gRmlsbSB6dSBiZXdlcnRlbiwgd2VubiBkaWVzZSBhdWNoIHdpcmtsaWNoIGd1dCBpc3QuIERhcyBSYXRpbmcgNSBrb21tdCBhbSBkcml0dGjDpHVmaWdzdGVuIHZvci4KCiMjIyAxLjIuMiBWZXJ0ZWlsdW5nIGRlciBLdW5kZW5yYXRpbmdzIG5hY2ggR2VucmUKYGBge3IsIGZpZy53aWR0aCA9IDEwLCBmaWcuaGVpZ2h0ID0gMTB9Ck1vdmllTGVuc2VFREFfSm9pbmVkJHJhdGluZyA8LSBhcy5pbnRlZ2VyKE1vdmllTGVuc2VFREFfSm9pbmVkJHJhdGluZykKCk1vdmllTGVuc2VFREFfSm9pbmVkICU+JSAKICBzZWxlY3QoLWMoIml0ZW0iLCAidXNlciIsICJ5ZWFyIiwgInVybCIpKSAlPiUgCiAgcGl2b3RfbG9uZ2VyKGNvbHM9YygidW5rbm93biIsICJBY3Rpb24iLCAiQWR2ZW50dXJlIiwgIkFuaW1hdGlvbiIsICJDaGlsZHJlbidzIiwKICAgICAgICAgICAgICAgICAgICAgICJDb21lZHkiLCAiQ3JpbWUiLCAiRG9jdW1lbnRhcnkiLCAiRHJhbWEiLCAiRmFudGFzeSIsCiAgICAgICAgICAgICAgICAgICAgICAiRmlsbS1Ob2lyIiwgIkhvcnJvciIsICJNdXNpY2FsIiwgIk15c3RlcnkiLCAiSG9ycm9yIiwKICAgICAgICAgICAgICAgICAgICAgICJNdXNpY2FsIiwgIk15c3RlcnkiLCAiUm9tYW5jZSIsICJTY2ktRmkiLCAiVGhyaWxsZXIiLAogICAgICAgICAgICAgICAgICAgICAgIldhciIsICJXZXN0ZXJuIiksCiAgICAgICAgICAgICAgIG5hbWVzX3RvID0gIkdlbnJlIiwgdmFsdWVzX3RvID0gImlzX2dlbnJlIikgJT4lCiAgZmlsdGVyKGlzX2dlbnJlID09IDEpICU+JSAKICBnZ3Bsb3QoYWVzKHggPSByYXRpbmcpKSArCiAgZ2VvbV9iYXIoZmlsbCA9ICJsaWdodGJsdWUiLCBjb2xvciA9ICJibGFjayIpICsKICBsYWJzKHggPSAiUmF0aW5ncyIsIAogICAgICAgeSA9ICJBbnphaGwiLCAKICAgICAgIHRpdGxlID0gIlZlcnRlaWx1bmcgZGVyIEt1bmRlbnJhdGluZ3MgbmFjaCBHZW5yZSIsCiAgICAgICBzdWJ0aXRsZSA9IHBhc3RlKCJHZXNhbXRlIEFuemFobCBLdW5kZW5yYXRpbmdzOiIsIGRpbShNb3ZpZUxlbnNlRURBX0pvaW5lZClbMV0pKSArIAogIGZhY2V0X3dyYXAofkdlbnJlKQpgYGAKSW4gZGVyIFZpc3VhbGlzaWVydW5nIGRlciBWZXJ0ZWlsdW5nIGRlciBLdW5kZW5yYXRpbmdzIHBybyBHZW5yZSBlcmtlbm5lbiB3aXIgYW5hbG9nLCB3aWUgYmVpIGRlciBWZXJ0ZWlsdW5nIGRlciBnZXNhbXRoYWZ0ZW4gS3VuZGVucmF0aW5ncywgZGFzcyBkaWUgUmF0aW5nIDMgdW5kIDQgYW0gbWVpc3RlbiB2ZXJnZWJlbiB3ZXJkZW4uIERpZXNlcyBNdXN0ZXIgaXN0IGJlaSBmYXN0IGFsbGVuIEdlbnJlcyBlcmtlbm5iYXIsIGVpbmZhY2ggbWl0IHVudGVyc2NoaWVkbGljaGVyIEludGVuc2l0w6R0IChBbnphaGwgUmF0aW5ncykuIAoKIyMgMS4zIFdpZSB2ZXJ0ZWlsZW4gc2ljaCBkaWUgbWl0dGxlcmVuIEt1bmRlbnJhdGluZ3MgcHJvIEZpbG0/CmBgYHtyfQojIERhdGFmcmFtZQpNb3ZpZUxlbnNlRURBICU+JSAKICBncm91cF9ieShpdGVtKSAlPiUgCiAgc3VtbWFyaXplKG1lYW5fcmF0aW5nX3Blcl9maWxtID0gbWVhbihyYXRpbmcpLAogICAgICAgICAgICBuX3JhdGluZ19wZXJfZmlsbSA9IG4oKSkgJT4lIAogIGFycmFuZ2Uobl9yYXRpbmdfcGVyX2ZpbG0pCgojIFZpc3VhbGlzaWVydW5nCk1vdmllTGVuc2VFREEgJT4lIAogIGdyb3VwX2J5KGl0ZW0pICU+JSAKICBzdW1tYXJpemUobWVhbl9yYXRpbmdfcGVyX2ZpbG0gPSBtZWFuKHJhdGluZykpICU+JSAKICBnZ3Bsb3QoYWVzKHggPSBtZWFuX3JhdGluZ19wZXJfZmlsbSkpICsgCiAgZ2VvbV9oaXN0b2dyYW0oY29sb3IgPSAiYmxhY2siLCBmaWxsID0gImxpZ2h0Ymx1ZSIsIGJpbndpZHRoID0gMC4xKSArCiAgICBsYWJzKHggPSAiUmF0aW5ncyIsIAogICAgICAgeSA9ICJBbnphaGwiLCAKICAgICAgIHRpdGxlID0gIlZlcnRlaWx1bmcgZGVyIE1pdHRsZXJlbiBLdW5kZW5yYXRpbmdzIHBybyBGaWxtIiwKICAgICAgIHN1YnRpdGxlID0gcGFzdGUoIkdlc2FtdGUgQW56YWhsIEt1bmRlbnJhdGluZ3M6IiwgZGltKE1vdmllTGVuc2VFREEpWzFdKSkKYGBgCldpciBlcmtlbm5lbiBpbSBQbG90IGRpZSBWZXJ0ZWlsdW5nIGR1cmNoc2Nobml0dGxpY2hlIFJhdGluZyBwcm8gRmlsbS4gQXVjaCBoaWVyIGlzdCBlcmtlbm5iYXIsIGRhc3MgZGllIG1laXN0ZW4gUmF0aW5ncyB6d2lzY2hlbiAzIHVuZCA0IGxpZWdlbi4gRWluZW4gQXVzcmVpc3NlciBnaWJ0IGVzIGJlaW0gUmF0aW5nIDEuIEJlaSBkZW4gbmF0w7xybGljaGVuL2dhbnp6w6RobGlnZW4gWmFobGVuIGVya2VubmVuIHdpciBlaW4gw7xiZXJyYXNjaGVuZGVzIE11c3RlcjogRGllIEFuemFobCBlcnNjaGVpbnQgamV3ZWlscyBow7ZoZXIgYWxzIGJlaSBkZW4gdW1saWVnZW5kZW4gUmF0aW5ncyBtaXQgS29tbWFzdGVsbGVuLiBEaWVzIGxpZWd0IGRhcmFuLCBkYXNzIGVzIEZpbG1lIGdpYnQgZGllIG51ciBlaW5lIG9kZXIgd2VuaWdlIEJld2VydHVuZ2VuIGJla29tbWVuIGhhYmVuIChzaWVoZSBhdXNnZWdlYmVuZXMgRGF0YWZhcm1lKS4KCiMjIDEuNCBXaWUgc3Rhcmsgc3RyZXVlbiBkaWUgUmF0aW5ncyB2b24gaW5kaXZpZHVlbGxlbiBLdW5kZW4/CmBgYHtyLCB3YXJuaW5nPUZBTFNFfQpNb3ZpZUxlbnNlRURBICU+JSBmaWx0ZXIodXNlciA9PSBjKDE6OSkpICU+JSAKICBnZ3Bsb3QoYWVzKHggPSB1c2VyLCB5ID0gcmF0aW5nKSkgKwogIGdlb21fdmlvbGluKGNvbG9yID0gImJsYWNrIiwgZmlsbCA9ICJsaWdodGJsdWUiKSArCiAgbGFicyh4ID0gIlVzZXIiLCAKICAgICAgIHkgPSAiUmF0aW5ncyIsIAogICAgICAgdGl0bGUgPSAiU3RyZXVldW5nIGRlciBSYXRpbmdzIHZvbiBpbmRpdmlkdWVsbGVuIEt1bmRlbiIsCiAgICAgICBzdWJ0aXRsZSA9ICJNb3ZpZUxlbnNlRGF0YSwgS3VuZGVuIDEtOSIpCmBgYApJbSBWaW9saW5lbnBsb3Qgc3RlbGxlbiB3aXIgZGllIGVyc3RlbiA5IFVzZXIgdW5kIGRlcmVuIFJhdGluZyBWZXJ0ZWlsdW5nZW4gZGFyLiBXaXIgZXJrZW5uZW4gaW0gUGxvdCwgZGFzcyBVc2VyIDIgdW5kIDggRmlsbWUgc2VociDDpGhubGljaGUgYmV3ZXJ0ZW4uIEJlaWRlIGJld2VydGVuIEZpbG1lIMO2ZnRlcnMgbWl0IGVpbmVyIDQgdW5kIGVoZXIgd2VuaWdlciBlaW5lIDMgdW5kIDUsIGFiZXIgbmllIDIgdW5kIDEuIFVzZXIgNSB1bmQgOSBiZXdlcnRlbiBGaWxtZSBoaW5nZWdlbiBpbSBnYW56ZW4gQmVyZWljaC4KCiMjIDEuNSBXZWxjaGVuIEVpbmZsdXNzIGhhdCBkaWUgTm9ybWllcnVuZyBkZXIgUmF0aW5ncyBwcm8gS3VuZGUgYXVmIGRlcmVuIFZlcnRlaWx1bmc/CmBgYHtyLCB3YXJuaW5nPUZBTFNFfQpNb3ZpZUxlbnNlbm9ybWFsaXplZCA8LSBub3JtYWxpemUoTW92aWVMZW5zZSkKTW92aWVMZW5zZUVEQV9Ob3JtYWxpemVkIDwtIChhcyhNb3ZpZUxlbnNlbm9ybWFsaXplZCwgImRhdGEuZnJhbWUiKSkKCk1vdmllTGVuc2VFREFfTm9ybWFsaXplZCAlPiUgZmlsdGVyKHVzZXIgPT0gYygxOjkpKSAlPiUgCiAgZ2dwbG90KGFlcyh4ID0gdXNlciwgeSA9IHJhdGluZykpICsKICBnZW9tX3Zpb2xpbihjb2xvciA9ICJibGFjayIsIGZpbGwgPSAibGlnaHRibHVlIikgKwogIGxhYnMoeCA9ICJVc2VyIiwgCiAgICAgICB5ID0gIk5vcm1hbGlzaWVydGUgUmF0aW5ncyIsIAogICAgICAgdGl0bGUgPSAiTm9ybWFsaXNpZXJ0ZSBTdHJldWV1bmcgZGVyIFJhdGluZ3Mgdm9uIGluZGl2aWR1ZWxsZW4gS3VuZGVuIiwKICAgICAgIHN1YnRpdGxlID0gIk1vdmllTGVuc2VEYXRhLCBLdW5kZW4gMSAtIDkiKQpgYGAKRsO8ciBkaWUgTm9ybWllcnVuZyBkZXIgRGF0ZW4gaGFiZW4gd2lyIGRpZSBGdW5rdGlvbiB2b24gUmVjb21tZW5kZXJsYWIgdmVyd2VuZGV0LiBEZXIgTWl0dGVsd2VydCBkZXIgUmF0aW5ncyBwcm8gVXNlciBiZXRyw6RndCBudW4gTnVsbC4gSW0gUGxvdCB2ZXJzY2hpZWJ0IHNpY2ggbnVuIG5pY2h0IG51ciBkaWUgeS1BY2hzZSwgc29uZGVybiBhdWNoIGRpZSBCYW5kYnJlaXRlLiB1c2VyIDUgdW5kIDksIGRpZSBhdWYgZGVuIFJvaGRhdGVuIDEtNSBiZXdlcnRldCBoYWJlbiwgaGFiZW4gbnVuIHVudGVyc2NoaWVkbGljaGUgQmFuZGJyZWl0ZW4uIERpZXMgbGllZ3QgZGFyYW4sIGRhc3MgZGVyIE1pdHRlbHdlcnQgZGVyIGJlaWRlbiBVc2VyIHVudGVyc2NoaWVkbGljaCBpc3QuCgojIyAxLjYgV2VsY2hlIHN0cnVrdHVyZWxsZW4gQ2hhcmFrdGVyaXN0aWthICh6LkIuIFNwYXJzaXR5KSB1bmQgQXVmZsOkbGxpZ2tlaXRlbiB6ZWlndCBkaWUgVXNlci1JdGVtIE1hdHJpeD8KYGBge3J9CmltYWdlKHggPSBNb3ZpZUxlbnNlLCAKICAgICAgeGxhYiA9ICJJdGVtcyIsIAogICAgICB5bGFiID0gIlVzZXJzIiwgCiAgICAgIG1haW4gPSAiU3BhcmlzdHkgOTQzIHggMTY2NCBVc2VyLUl0ZW0gTWF0cml4IDk0MyB4IDE2NjQiKSAKCmltYWdlKE1vdmllTGVuc2VbMTo1MCwxOjUwXSwKICAgICAgeGxhYiA9ICJJdGVtcyIsCiAgICAgIHlsYWIgPSAiVXNlcnMiLCAKICAgICAgbWFpbiA9ICJTcGFyaXN0eSA1MCB4IDUwIFVzZXItSXRlbSBNYXRyaXgiKQoKIyBucmF0aW5ncyhNb3ZpZUxlbnNlKSB6YWVobHQgZGllIEFuemFobCB2b3JoYW5kZW5lbiBLb21iaW5hdGlvbmVuIHZvbiBVc2VyIHVuZCBJdGVtcwoobnJhdGluZ3MoTW92aWVMZW5zZSkgLyAoZGltKE1vdmllTGVuc2UpWzFdICogZGltKE1vdmllTGVuc2UpWzJdKSAqIDEwMCkKYGBgCkbDvHIgZGllIERhcnN0ZWxsdW5nIGRlciBTcGFyc2l0eSBoYWJlbiB3aXIgZGllIGltYWdlIEZ1bmt0aW9uIHZvbiBSZWNvbW1lbmRlcmxhYiB2ZXJ3ZW5kZXQuIEplZGUgWmVpbGUgdm9uIE1vdmllTGVuc2UgZW50c3ByaWNodCBlaW5lbSBCZW51dHplciB1bmQgamVkZSBTcGFsdGUgZWluZW0gRmlsbSB1bmQgZsO8ciBqZWRlIGdlc2NoYXV0ZSBLb21iaW5hdGlvbiB3aXJkIGVpbiBQaXhlbCBpbiBHcmF1c3R1ZmVuLCBqZSBuYWNoIFJhdGluZywgbWFya2llcnQuIEltIGVyc3RlbiBQbG90IHdpcmQgZXJzaWNodGxpY2gsIGRhc3MgZGllIGVyc3RlbiBVc2VyIHdlbmlnZXIgRmlsbWUgYmV3ZXJ0ZXQgaGFiZW4sIGRlbm4gb2JlbiByZWNodHMgc2luZCBrZWluZSBQdW5rdGUgbWVociBlcnNpY2h0bGljaC4gQXVjaCBpc3QgYXVmZsOkbGxpZywgZGFzcyBkaWUgZXJzdGVuIGV0d2EgNTAwIGjDpHVmaWdlciBnZXNjaGF1dCB3dXJkZW4sIGRlbm4gYmlzIHp1IGRpZXNlbSBCZXJlaWNoIHNpbmQgYW0gbWVpc3RlbiBQaXhlbCBlaW5nZWbDpHJidC4gVW0gZGllIERhcnN0ZWxsdW5nIGdlbmF1IHZlcnN0ZWhlbiB6dSBrw7ZubmVuLCBoYWJlbiB3aXIgaW0gendlaXRlbiBQbG90IG51ciBkaWUgZXJzdGVuIDUwIFVzZXIgdW5kIEl0ZW1zIGRhcmdlc3RlbGx0LiBEb3J0IGlzdCBkaWUgaG9oZSBTcGFyc2l0eSBndXQgZXJrZW5uYmFyLgpHZXNhbXRoYWZ0IGdpYnQgZXMgOTQzIHggMTY2NCA9IDHigJk1NjnigJkxNTIgS29tYmluYXRpb25lbiB6d2lzY2hlbiBVc2VyIHVuZCBGaWxtLiBBbGxlcmRpbmdzIGhhdCBuaWNodCBqZWRlciBOdXR6ZXIgamVkZW4gRmlsbSBnZXNlaGVuLCBhdXMgZGllc2VtIEdydW5kIGlzdCBlcyB3aWNodGlnIGRpZSBzcGFyc2l0eSBkZXIgTWF0cml4IHp1IGJldHJhY2h0ZW4uIEluIE1vdmllTGVuc2UgTWF0cml4IGZlaGxlbiBjYS4gOTQlIGRlciBLb21iaW5hdGlvbmVuLiBOdXIgZsO8ciA2LjMlIGRlciBtw7ZnbGljaGVuIEtvbWJpbmF0aW9uZW4gc2luZCBSYXRpbmdzIHZvcmhhbmRlbi4KCiMgMiBEYXRlbnJlZHVrdGlvbgpBdWZnYWJlIDI6IFJlZHV6aWVyZSBkZW4gTW92aWVMZW5zZSBEYXRlbnNhdHogYXVmIHJ1bmQgNDAwIEt1bmRlbiB1bmQgNzAwIEZpbG1lLCBpbmRlbSBkdSBGaWxtZSB1bmQgS3VuZGVuIG1pdCBzZWhyIHdlbmlnZW4gUmF0aW5ncyBlbnRmZXJuc3QuCgojIyAyLjEgVm9yYmVyZWl0dW5nCiMjIyAyLjEuMSBEYXRhRnJhbWUgbmV1IGVpbmxlc2VuCmBgYHtyfQpNb3ZpZUxlbnNlVG9DdXQgPC0gYXMoTW92aWVMZW5zZSwgImRhdGEuZnJhbWUiKQpNb3ZpZUxlbnNlVG9DdXQKYGBgCgojIyMgMi4xLjIgQXVzd2FobCBkZXIgNDAwIEt1bmRlbgpgYGB7cn0Kc2VsZWN0X3VzZXJfNDAwIDwtIGZ1bmN0aW9uKG1vdmllX2RmLCBzdGFydCwgZW5kKSB7CiAgc2VsZWN0ZWRfdXNlciA8LSBtb3ZpZV9kZiAlPiUgCiAgICBncm91cF9ieSh1c2VyKSAlPiUgCiAgICBzdW1tYXJpemUoQW56YWhsID0gbigpKSAlPiUgCiAgICBhcnJhbmdlKGRlc2MoQW56YWhsKSkgJT4lIAogICAgc2xpY2Uoc3RhcnQ6ZW5kKQogIHNlbGVjdGVkX3VzZXIKfQoKTW92aWVMZW5zZTQwMFVzZXJfMSA8LSBzZWxlY3RfdXNlcl80MDAoTW92aWVMZW5zZVRvQ3V0LCAwLCA0MDApCk1vdmllTGVuc2U0MDBVc2VyXzEKCk1vdmllTGVuc2U0MDBVc2VyXzIgPC0gc2VsZWN0X3VzZXJfNDAwKE1vdmllTGVuc2VUb0N1dCwgMjAwLCA1OTkpCk1vdmllTGVuc2U0MDBVc2VyXzIKYGBgCgpCZWkgZGVyIEF1c3dhaGwgZGVyIDQwMCBVc2VyIGhhYmVuIHdpciBkaXJla3QgYXVjaCB6d2VpIERhdGFmcmFtZXMgZXJzdGVsbHQsIGRhIHdpciBkaWUgTUMgenUgendlaXQgYmVhcmJlaXRlbi4gRsO8ciBQZXJzb24gMSBoYWJlbiB3aXIgZGllIDQwMCBVc2VyIG1pdCBkZW4gbWVpc3RlbiBSYXRpbmdzIGF1c2dld8OkaGx0IHVuZCBmw7xyIFBlcnNvbiAyIFVzZXIgMjAwIGJpcyA2MDAuIFdpciBoYWJlbiBkaWVzZXMgVm9yZ2VoZW4gZ2V3w6RobHQgdW0gc2ljaGVyenVzdGVsbGVuLCBkYXNzIG51ciBlaW5lIFRlaWwgZGVyIFVzZXIgaW4gYmVpZGVuIERhdGFmcmFtZXMgZW50aGFsdGVuIGlzdC4gQWx0ZXJuYXRpdiBow6R0dGVuIHdpciB2b24gZGVuIFRvcCA1MDAgVXNlciB6dWbDpGxsaWcgODAlIGbDvHIgUGVyc29uIDEgdW5kIDIgdmVyd2VuZGV0LCBkYW5uIGjDpHR0ZSBkaWUgw5xiZXJsYXBwdW5nIGFiZXIgc2VociBob2NoIHNlaW4ga8O2bm5lbiwgc28gaXN0IGVzIG51ciBkaWUgSMOkbGZ0LgoKIyMjIDIuMS4zIEF1c3dhaGwgZGVyIDcwMCBNb3ZpZXMKYGBge3J9CnNlbGVjdF9pdGVtXzcwMCA8LSBmdW5jdGlvbihtb3ZpZV9kZiwgc3RhcnQsIGVuZCkgewogIHNlbGVjdGVkX2l0ZW0gPC0gTW92aWVMZW5zZVRvQ3V0ICU+JSAKICBncm91cF9ieShpdGVtKSAlPiUgCiAgc3VtbWFyaXNlKEFuemFobCA9IG4oKSkgJT4lIAogIGFycmFuZ2UoZGVzYyhBbnphaGwpKSAlPiUgCiAgc2xpY2Uoc3RhcnQ6ZW5kKQp9CgpNb3ZpZUxlbnNlNzAwSXRlbXNfMSA8LSBzZWxlY3RfaXRlbV83MDAoTW92aWVMZW5zZVRvQ3V0LCAwLCA3MDApCk1vdmllTGVuc2U3MDBJdGVtc18xCgpNb3ZpZUxlbnNlNzAwSXRlbXNfMiA8LSBzZWxlY3RfaXRlbV83MDAoTW92aWVMZW5zZVRvQ3V0LCAxNTAsIDg0OSkKTW92aWVMZW5zZTcwMEl0ZW1zXzIKCmBgYAoKRGFzIGdsZWljaGUgVm9yZ2VoZW4gaGFiZW4gd2lyIGJlaSBkZW4gRmlsbWVuIGdld8OkaGx0LiAKCiMjIyAyLjEuNCBEYXRhRnJhbWUgc2NobmVpZGVuCmBgYHtyfQpkZl9jdXR0ZXIgPC0gZnVuY3Rpb24obW92aWVfZGYsIHNlbGVjdGVkX3VzZXIsIHNlbGVjdGVkX2l0ZW1zKSB7CiAgbW92aWVfZGZfY3V0IDwtIG1vdmllX2RmICU+JQogICAgICBmaWx0ZXIodXNlciAlaW4lIGMoc2VsZWN0ZWRfdXNlciR1c2VyKSkKICBtb3ZpZV9kZl9jdXQgPC0gbW92aWVfZGZfY3V0ICU+JSAKICAgICAgZmlsdGVyKGl0ZW0gJWluJSBjKHNlbGVjdGVkX2l0ZW1zJGl0ZW0pKQogIG1vdmllX2RmX2N1dAp9CgpNb3ZpZUxlbnNlQ3V0XzEgPC0gZGZfY3V0dGVyKE1vdmllTGVuc2VUb0N1dCwgTW92aWVMZW5zZTQwMFVzZXJfMSwgTW92aWVMZW5zZTcwMEl0ZW1zXzEpCk1vdmllTGVuc2VDdXRfMQoKTW92aWVMZW5zZUN1dF8yIDwtIGRmX2N1dHRlcihNb3ZpZUxlbnNlVG9DdXQsIE1vdmllTGVuc2U0MDBVc2VyXzIsIE1vdmllTGVuc2U3MDBJdGVtc18yKQpNb3ZpZUxlbnNlQ3V0XzIKYGBgCgpVbnRlcnN1Y2hlIHVuZCBkb2t1bWVudGllcmUgZGllIEVpZ2Vuc2NoYWZ0ZW4gZGVzIHJlZHV6aWVydGVuIERhdGVuc2F0emVzIHVuZCBiZXNjaHJlaWJlIGRlbiBFZmZla3QgZGVyIERhdGVucmVkdWt0aW9uLCBkLmguCgojIyAyLjIgQW56YWhsIEZpbG1lIHVuZCBLdW5kZW4gc293aWUgU3BhcnNpdHkgdm9yIHVuZCBuYWNoIERhdGVucmVkdWt0aW9uLAojIyMgMi4yLjEgVm9yIGRlciBEYXRlbnJlZHVrdGlvbgpgYGB7cn0KaW1hZ2UoTW92aWVMZW5zZSwgCiAgICAgIHhsYWIgPSAiSXRlbXMiLCAKICAgICAgeWxhYiA9ICJVc2VycyIsIAogICAgICBtYWluID0gIlZvciBEYXRlbnJlZHVrdGlvbiwgVXNlci1JdGVtIE1hdHJpeCA5NDMgeCAxNjY0IikgCgpzcGFyc2l0eV90ZXh0IDwtIGZ1bmN0aW9uKHJlYWxyYXRpbmdfbWF0cml4KSB7CiAgcHJpbnQocGFzdGUoIkFuemFobCB2b3JoYW5kZW5lIFVzZXItSXRlbSBSYXRpbmcgaW4iLCBucmF0aW5ncyhyZWFscmF0aW5nX21hdHJpeCkgLyAoZGltKHJlYWxyYXRpbmdfbWF0cml4KVsxXSAqIGRpbShyZWFscmF0aW5nX21hdHJpeClbMl0pICogMTAwLCAiJSIpKQogIHByaW50KHBhc3RlKCJTcGFyc2l0eSBkZXIgTWF0cml4IiwgMTAwIC0gKG5yYXRpbmdzKHJlYWxyYXRpbmdfbWF0cml4KSAvIChkaW0ocmVhbHJhdGluZ19tYXRyaXgpWzFdICogZGltKHJlYWxyYXRpbmdfbWF0cml4KVsyXSkgKiAxMDApLCAiJSIpKQp9CgpzcGFyc2l0eV90ZXh0KE1vdmllTGVuc2UpCgpgYGAKWnVyIFJlcGV0aXRpb24gc3RlbGxlbiB3aXIgbm9jaG1hbHMgZGllIFNwYXJzaXR5IGFscyBCaWxkIGRhciB1bmQgYmVyZWNobmVuIGRlbiBXZXJ0LgoKCiMjIyAyLjIuMiBOYWNoIGRlciAxLiBEYXRlbnJlZHVrdGlvbgpgYGB7cn0KTW92aWVMZW5zZUNvbXBhY3RfMSA8LSBhcyhNb3ZpZUxlbnNlQ3V0XzEsICJyZWFsUmF0aW5nTWF0cml4IikKaW1hZ2UoTW92aWVMZW5zZUNvbXBhY3RfMSwKICAgICAgeGxhYiA9ICJJdGVtcyIsIAogICAgICB5bGFiID0gIlVzZXJzIiwgCiAgICAgIG1haW4gPSAiTmFjaCBEYXRlbnJlZHVrdGlvbiAxLCBVc2VyLUl0ZW0gTWF0cml4IDQwMCB4IDcwMCIpCgpzcGFyc2l0eV90ZXh0KE1vdmllTGVuc2VDb21wYWN0XzEpCmBgYAoKRsO8ciBkZW4gZXJzdGVuIERhdGVuc2F0eiB3aXJkIGVyc2ljaHRsaWNoLCBkYXNzIGRpZSBSYXRpbmdzIGdlZ2Vuw7xiZXIgZGVtIHVyc3Byw7xuZ2xpY2hlbiBEYXRlbnNhdHogZ2xlaWNobcOkc3NpZyB2ZXJ0ZWlsdCBzaW5kLiBWZXJlaW56ZWx0IHNpbmQgZsO8ciBVc2VyICh6LkIuIGltIEJlcmVpY2ggOTAtMTUwKSB1bmQgSXRlbXMgKHouQiBCZXJlaWNodCB1bSA2MDApIGR1bmtsZXJlIEJlcmVpY2hlIGVya2VubmJhci4gSW4gZGllc2VuIGTDvHJmdGVuIGRpZSBSYXRpbmdzIGjDtmhlciB1bmQgU3BhcnNpdHkgZ2VyaW5nZXIgc2Vpbi4KRGllIFNwYXJzaXR5IGJldHLDpGd0IG51biBhdWNoIG51ciBub2NoIGV0d2EgNzUlIHVuZCBmw7xyIDI1JSBkZXIgbcO2Z2xpY2hlbiBLb21iaW5hdGlvbmVuIHp3aXNjaGVuIFVzZXIgdW5kIEl0ZW0gd3VyZGVuIFJhdGluZ3MgYW5nZWdlYmVuLgoKRGllc2Ugc3RhcmtlIMOEbmRlcnVuZyB3YXIgYWJlciB6dSBlcndhcnRlbiwgZGEgd2lyIGRpZSBVc2VyIHVuZCBJdGVtcyBtaXQgZGVuIG1laXN0ZW4gUmF0aW5ncyBhdXNnZXfDpGhsdCBoYWJlbi4KCiMjIyAyLjIuMyBOYWNoIGRlciAyLiBEYXRlbnJlZHVrdGlvbgpgYGB7cn0KTW92aWVMZW5zZUNvbXBhY3RfMiA8LSBhcyhNb3ZpZUxlbnNlQ3V0XzIsICJyZWFsUmF0aW5nTWF0cml4IikKaW1hZ2UoTW92aWVMZW5zZUNvbXBhY3RfMiwKICAgICAgeGxhYiA9ICJJdGVtcyIsIAogICAgICB5bGFiID0gIlVzZXJzIiwgCiAgICAgIG1haW4gPSAiTmFjaCBEYXRlbnJlZHVrdGlvbiAyLCBVc2VyLUl0ZW0gTWF0cml4IDQwMCB4IDcwMCIpCgpzcGFyc2l0eV90ZXh0KE1vdmllTGVuc2VDb21wYWN0XzIpCgpgYGAKRsO8ciBkZW4gendlaXRlbiBEYXRlbnNhdHogd2lyZCBlcnNpY2h0bGljaCwgZGFzcyBkaWUgUmF0aW5ncyBnZWdlbsO8YmVyIGRlbSB1cnNwcsO8bmdsaWNoZW4gRGF0ZW5zYXR6IGdsZWljaG3DpHNzaWcgdmVydGVpbHQgc2luZCwgZ2VnZW7DvGJlciBkZW0gZXJzdGVuIERhdGVuc2F0eiBhYmVyIHNpY2h0YmFyIHdlbmlnZXIgUmF0aW5ncyB2b3JoYW5kZW4gc2luZC4gRHVua2xlcmUgQmVyZWljaCwgd2llIGJlaSBEYXRlbnNhdHoxIHNpbmQga2F1bSBtZWhyIHp1IGVya2VubmVuLgpEaWUgU3BhcnNpdHkgYmV0csOkZ3QgbGllZ3QgbnVuIGJlaSA5My42JSwgc2llIGlzdCBnZWdlbsO8YmVyIGRlbSBlcnN0ZW4gRGF0ZW5zYXR6IGFsc28gZGV1dGxpY2ggYW5nZXN0aWVnZW4sIGxpZWd0IGFiZXIgYmVyZWl0cyBpbSBCZXJlaWNoIGRlcyB1cnNwcsO8bmdsaWNoZW4gV2VydGVzLgpEaWVzZXIgQW5zdGllZyB3YXIgenUgZXJ3YXJ0ZW4sIGRhIHdpciBuaWNodCBtZWhyIGRpZSBVc2VyIHVuZCBJdGVtcyBtaXQgZGVuIG1laXN0ZW4gUmF0aW5ncyBhdXNnZXfDpGhsdCBoYWJlbiwgc29uZGVybiB6LkIuIGJlaSBkZW4gVXNlcm4gYmVpIFRvcCAyMDAgYW5nZWZhbmdlbiBoYWJlbi4KCiMjIDIuMyBNaXR0bGVyZSBLdW5kZW5yYXRpbmdzIHBybyBGaWxtIHZvciB1bmQgbmFjaCBEYXRlbnJlZHVrdGlvbiwKYGBge3J9Cm1lYW5fcmF0aW5nX3Blcl9maWxtX3ZpeiA8LSBmdW5jdGlvbihtb3ZpZV9kZikgewogIG1vdmllX2RmICU+JSAKICBncm91cF9ieShpdGVtKSAlPiUgCiAgc3VtbWFyaXplKG1lYW5fcmF0aW5nX3Blcl9maWxtID0gbWVhbihyYXRpbmcpKSAlPiUgCiAgZ2dwbG90KGFlcyh4ID0gbWVhbl9yYXRpbmdfcGVyX2ZpbG0pKSArIAogIGdlb21faGlzdG9ncmFtKGNvbG9yID0gImJsYWNrIiwgZmlsbCA9ICJsaWdodGJsdWUiLCBiaW53aWR0aCA9IDAuMSkgKwogICAgbGFicyh4ID0gIlJhdGluZ3MiLCAKICAgICAgIHkgPSAiQW56YWhsIiwgCiAgICAgICB0aXRsZSA9ICJNaXR0bGVyZSBLdW5kZW5yYXRpbmdzIFZlcnRlaWx1bmciLAogICAgICAgc3VidGl0bGUgPSBwYXN0ZSgiR2VzYW10ZSBBbnphaGwgS3VuZGVucmF0aW5nczoiLCBkaW0obW92aWVfZGYpWzFdKSkgKwogICAgZ2VvbV92bGluZSh4aW50ZXJjZXB0ID0gbWVhbihtb3ZpZV9kZiRyYXRpbmcpLCBjb2xvciA9ICJyZWQiLCBsaW5ldHlwZSA9ICJkYXNoZWQiLCBzaXplID0gMC41KQp9CiMgVm9yIHJlZHVrdGlvbgpwcmludChtZWFuX3JhdGluZ19wZXJfZmlsbV92aXooTW92aWVMZW5zZUVEQSkpCiMgbmFjaCAxLiBSZWR1dGtpb24KcHJpbnQobWVhbl9yYXRpbmdfcGVyX2ZpbG1fdml6KE1vdmllTGVuc2VDdXRfMSkpCiMgbmFjaCAyLiBSZWR1a3Rpb24KcHJpbnQobWVhbl9yYXRpbmdfcGVyX2ZpbG1fdml6KE1vdmllTGVuc2VDdXRfMikpCmBgYApEaWUgZXJzdGUgVmlzdWFsaXNpZXJ1bmcgemVpZ3QgZGVuIGJlcmVpdHMgYmVrYW5udGVuIFBsb3QgbWl0IGRlbiBtaXR0bGVyZW4gS3VuZGVucmF0aW5ncyBmw7xyIGRlbiBnZXNhbXRlbiBEYXRlbnNhdHouIFBsb3QgMiB1bmQgMyB6ZWlndCBkaWUgc2VsYmUgQXVzd2VydHVuZyBmw7xyIGRpZSBiZWlkZW4gZ2Vrw7xyenRlbiBEYXRlbnPDpHR6ZS4KSW4gZGVuIFZpc3VhbGlzaWVydW5nZW4gZXJrZW5uZW4gd2lyLCBkYXNzIGRlciBNaXR0ZWx3ZXJ0IGRlciBLdW5kZW5yYXRpbmcgZsO8ciBkZW4gdXJzcHLDvG5nbGljaGVuLCBzb3dpZSBhdWNoIGbDvHIgZGllIGJlaWRlbiByZWR1emllcnRlbiBEYXRlbnPDpHR6ZSwgbmljaHQgZ3Jvc3NhcnRpZyDDpG5kZXJ0LiBEaWUgTWl0dGVsd2VydGUgYmVmaW5kZW4gc2ljaCBiZWkgYWxsZW4gaW0gQmVyZWljaCB2b24gMy41LiBXYXMgYWJlciBlcmtlbm5iYXIgd2lyZCwgaXN0LCBkYXNzIGJlaSBkZXIgMS4gUmVkdWt0aW9uIGRpZSBob2hlIEFuemFobCBSYXRpbmcgYmVpIGRlbiBuYXTDvHJsaWNoZW4vZ2FuenphaGxpZ2VuIFphaGxlbiB3ZWdnZWZhbGxlbiBpc3QuIFdlaXRlcmhpbiBzaW5kIGJlaSBhbGxlbiBWaXN1YWxpc2llcnVuZ2VuIGVya2VubmJhciwgZGFzcyBkaWUgbWVpc3RlbiBSYXRpbmcgaW0gQmVyZWljaCB2b24gMyBiaXMgNCBsaWVnZW4uIAoKIyMgMi40IEbDvHIgR3J1cHBlbjogUXVhbnRpZml6aWVyZSDigJxJbnRlcnNlY3Rpb24gb3ZlciBVbmlvbuKAnSBkZXIgUmF0aW5ncyBkZXIgdW50ZXJzY2hpZWRsaWNoIHJlZHV6aWVydGVuIERhdGVuc8OkdHplLgpgYGB7cn0KaW50ZXJzZWN0X2pvaW4gPC0gaW5uZXJfam9pbihNb3ZpZUxlbnNlQ3V0XzEsIE1vdmllTGVuc2VDdXRfMiwgYnkgPSBjKCJ1c2VyIiwgIml0ZW0iKSkKaW50ZXJzZWN0X2pvaW4KCnVuaW9uX2pvaW4gPC0gZnVsbF9qb2luKE1vdmllTGVuc2VDdXRfMSwgTW92aWVMZW5zZUN1dF8yLCBieSA9IGMoInVzZXIiLCAiaXRlbSIpKQp1bmlvbl9qb2luCgpwYXN0ZSgiRWluZSBJbnRlcnNlY3Rpb24gb3ZlciBVbmlvbiB2b24iLCBkaW0oaW50ZXJzZWN0X2pvaW4pWzFdIC8gZGltKHVuaW9uX2pvaW4pWzFdICogMTAwLCAiJSwgendpc2NoZW4gZGVuIGJlaWRlbiByZWR1emllcnRlbiBEYXRlbnPDpHR6ZW4iKQoKYGBgCgpadXIgQmVhbnR3b3J0dW5nIGRpZXNlciBGcmFnZSBoYWJlbiB3aXIgZWluZXJzZWl0cyBlaW5lbiBEYXRlbnNhdHogbWl0IERhdGVuLCBkaWUgaW4gYmVpZGVuIERhdGVuc8OkdHplbiB2b3JoYW5kZW4gc2luZCBlcnN0ZWxsdCB1bmQgZGllc2VuIG1pdCBkZXIgZ2VzYW10ZW4gQW56YWhsIERhdGVuIHZlcmdsaWNoZW4uIEVzIHplaWd0IHNpY2gsIGRhc3MgZXMgZWluZSDDnGJlcnNjaG5laWR1bmcgdm9uIDE1LjYlIHp3aXNjaGVuIGRlbiBiZWlkZW4gcmVkdXppZXJ0ZW4gRGF0ZW5zw6R0emVuIGdpYnQuIERpZXNlciBlaGVyIHRpZWZlIFdlcnQgw7xiZXJyYXNjaHQsIHdlaWwgei5CLiA1MCUgZGVyIFVzZXIgw7xiZXJlaW5zdGltbWVuLiBBYmVyIGF1ZmdydW5kIGRlciBob2hlbiBTcGFyc2l0eSBpc3QgZGllIMOcYmVyc2NobmVpZHVuZyBkZXIgRGF0ZW4gdmllbCB0aWVmZXIuCgojIDMgQW5hbHlzZSDDhGhubGljaGtlaXRzbWF0cml4CkF1ZmdhYmUgMzogRXJ6ZXVnZSBlaW5lbiBJQkNGIFJlY29tbWVuZGVyIHVuZCBhbmFseXNpZXJlIGRpZSDDhGhubGljaGtlaXRzbWF0cml4IGRlcyB0cmFpbmllcnRlbiBNb2RlbGxlcyBmw7xyIGRlbiByZWR1emllcnRlbiBEYXRlbnNhdHouCgojIyAzLjEgWmVybGVnZSBkZW4gcmVkdXppZXJ0ZW4gTW92aWVMZW5zZSBEYXRlbnNhdHogaW4gZWluIGRpc2p1bmt0ZXMgVHJhaW5pbmdzLXVuZCBUZXN0ZGF0ZW5zZXQgaW0gVmVyaMOkbHRuaXMgNDoxLApgYGB7cn0KdHJhaW5fdGVzdF9zcGxpdCA8LSBmdW5jdGlvbihtb3ZpZV9kZiwgc3BsaXQgPSAwLjgpIHsKICBuIDwtIGRpbShtb3ZpZV9kZilbMV0KICBuX3RyYWluIDwtIHJvdW5kKG4gKiBzcGxpdCkKICBuX3Rlc3QgPC0gbiAtIG5fdHJhaW4KICB0cmFpbmluZyA8LSBtb3ZpZV9kZlsxOm5fdHJhaW5dCiAgdGVzdCA8LSBtb3ZpZV9kZlsobl90cmFpbiArIDEpOm5dCiAgcmV0dXJuKGxpc3QodHJhaW5pbmcsIHRlc3QpKQp9Cgp0cmFpbl90ZXN0X2xpc3RfMSA8LSB0cmFpbl90ZXN0X3NwbGl0KE1vdmllTGVuc2VDb21wYWN0XzEpCnRyYWluaW5nXzEgPC0gdHJhaW5fdGVzdF9saXN0XzFbWzFdXQp0ZXN0XzEgPC0gdHJhaW5fdGVzdF9saXN0XzFbWzJdXQp0cmFpbmluZ18xCnRlc3RfMQoKdHJhaW5fdGVzdF9saXN0XzIgPC0gdHJhaW5fdGVzdF9zcGxpdChNb3ZpZUxlbnNlQ29tcGFjdF8yKQp0cmFpbmluZ18yIDwtIHRyYWluX3Rlc3RfbGlzdF8yW1sxXV0KdGVzdF8yIDwtIHRyYWluX3Rlc3RfbGlzdF8yW1syXV0KdHJhaW5pbmdfMgp0ZXN0XzIKCmBgYApCZWlkZSByZWR1emllcnRlbiBEYXRlbnPDpHR6ZSB3dXJkZW4gaW0gVmVyaMOkbHRuaXMgNDoxLCAoNCBUZWlsZSBUcmFpbmluZyB1bmQgMSBUZWlsIFRlc3QpIHJlZHV6aWVydC4KCiMjIDMuMiBUcmFpbmllcmUgZWluIElCQ0YgTW9kZWxsIG1pdCAzMCBOYWNoYmFybiB1bmQgQ29zaW5lIFNpbWlsYXJpdHkKYGBge3J9CnJpYmNmXzEgPC0gUmVjb21tZW5kZXIodHJhaW5pbmdfMSwgIklCQ0YiLCBwYXJhbT1saXN0KGs9IDMwLCBtZXRob2QgPSAiY29zaW5lIikpCnJpYmNmXzEKCnJpYmNmXzIgPC0gUmVjb21tZW5kZXIodHJhaW5pbmdfMiwgIklCQ0YiLCBwYXJhbT1saXN0KGs9IDMwLCBtZXRob2QgPSAiY29zaW5lIikpCnJpYmNmXzIKYGBgCkVzIHd1cmRlbiBqZXdlaWxzIGbDvHIgYmVpZGUgcmVkdXppZXJ0ZW4gRGF0ZW5zYWV0emUgZWluIElCQ0YgTW9kZWxsIG1pdCAzMCBOYWNoYmFybiB1bmQgZGVyIENvc2luZSBTaW1pbGFyaXR5IG1pdHRlbHMgZGVyIHZvbiBSZWNvbW1lbmRlcmxhYiB6dXIgVmVyZsO8Z3VuZyBnZXN0ZWxsdGVuIE1ldGhvZGUgdHJhaW5pZXJ0LiBEaWUgQXVzd2VydHVuZyBiZXN0w6R0aWd0LCBkYXNzIGRhcyBUcmFpbmluZyBtaXR0ZWxzIDMyMCBVc2VybiwgcmVzcC4gODAlIGRlciB1cnNwcsO8bmdsaWNoZW4gNDAwLCBkdXJjaGdlZsO8aHJ0IHd1cmRlLgoKIyMgMy4zIEJlc3RpbW1lIGRpZSBWZXJ0ZWlsdW5nIGRlciBGaWxtZSwgd2VsY2hlIGJlaSBJQkNGIGbDvHIgcGFhcndlaXNlIMOEaG5saWNoa2VpdHN2ZXJnbGVpY2hlIHZlcndlbmRldCB3ZXJkZW4KYGBge3J9CnJpYmNmX3NpbV9pdGVtX2RmIDwtIGZ1bmN0aW9uKHJpYmNmKSB7CiAgIyBtb2RlbAogIHJpYmNmX21vZGVsIDwtIGdldE1vZGVsKHJpYmNmKQogICMgZGF0YWZyYW1lIGVyc3RlbGxlbgogIHJpYmNmX3NpbV9kZiA8LSBhcy5kYXRhLmZyYW1lKGNvbFN1bXMocmliY2ZfbW9kZWwkc2ltID4gMCkpCiAgIyBJdGVtIGFscyBuZXVlIFNwYWx0ZSBoaW56dWZ1ZWdlbiB1bmQgSW5kZXggZW50ZmVybmVuCiAgcmliY2Zfc2ltX2RmXyA8LSBjYmluZChpdGVtID0gcm93bmFtZXMocmliY2Zfc2ltX2RmKSwgcmliY2Zfc2ltX2RmKQogIHJvd25hbWVzKHJpYmNmX3NpbV9kZl8pIDwtIE5VTEwKICAjIHJldHVybiBkZgogIHJpYmNmX3NpbV9kZl8KfQoKcmliY2Zfc2ltX3ZpeiA8LSBmdW5jdGlvbihyaWJjZl9zaW1fZGZfLCBuX3JlZHVjKSB7CiAgICByaWJjZl9zaW1fZGZfICU+JQogICAgcmVuYW1lKEFuemFobCA9IDIpICU+JSAKICAgIGdncGxvdChhZXMoeCA9IEFuemFobCkpICsgCiAgICBnZW9tX2hpc3RvZ3JhbShiaW53aWR0aCA9ICAxKSArCiAgICBsYWJzKHRpdGxlID0gIlZlcnRlaWx1bmcgZGVyIMOEaG5saWNoa2VpdHN2ZXJnbGVpY2hlIiwKICAgICAgICAgeCA9ICJBbnphaGwgRmlsbWUgYWxzIE5hY2hiYXIiLCAKICAgICAgICAgeSA9ICJBbnphaGwiLAogICAgICAgICBzdWJ0aXRsZSA9IHBhc3RlKCJyaWJjZiIsIG5fcmVkdWMpKQp9CgpyaWJjZl9zaW1fZGZfMSA8LSByaWJjZl9zaW1faXRlbV9kZihyaWJjZl8xKQpyaWJjZl9zaW1fdml6KHJpYmNmX3NpbV9kZl8xLCAxKQoKcmliY2Zfc2ltX2RmXzIgPC0gcmliY2Zfc2ltX2l0ZW1fZGYocmliY2ZfMikKcmliY2Zfc2ltX3ZpeihyaWJjZl9zaW1fZGZfMiwgMikKCgpgYGAKSW4gYmVpZGVuIEhpc3RvZ3JhbW1lbiBlcmtlbm5lbiB3aXIgYXVmIGRlciBYIEFjaHNlIGRpZSBBbnphaGwgRmlsbWUgZGllIGFscyBOYWNoYmFyIGJlaSBlaW5lbSBhbmRlcmVuIEZpbG0gdm9ya29tbWVuLiBNYW4gZXJrZW5udCBpbSBQbG90LCBkYXNzIGVzIHdlbmlnZSBGaWxtZSBnaWJ0LCBkaWUgaMOkdWZpZyB2aWVsZSBOYWNoYmFyZW4gaGFiZW4uIEJlaWRlIFBsb3RzIGZvbGdlbmUgZWluZXIgw6RobmxpY2hlbiBWZXJ0ZWlsdW5nLiAKCgojIyAzLjQgQmVzdGltbWUgZGllIEZpbG1lLCBkaWUgYW0gaMOkdWZpZ3N0ZW4gaW4gZGVyIENvc2luZS3DhGhubGljaGtlaXRzbWF0cml4IGF1ZnRhdWNoZW4gdW5kIGFuYWx5c2llcmUgZGVyZW4gVm9ya29tbWVuIHVuZCBSYXRpbmdzIGltIHJlZHV6aWVydGVuIERhdGVuc2F0ei4KCmBgYHtyLCB3YXJuaW5nPUZBTFNFfQp0b3BfMTBfaXRlbV9zaW0gPC0gZnVuY3Rpb24ocmliY2Zfc2ltX2RmXywgbl9yZWR1YykgewogIHJlc3VsdCA8LSByaWJjZl9zaW1fZGZfICU+JSAKICByZW5hbWUoQW56YWhsID0gMikgJT4lIAogIGFycmFuZ2UoZGVzYyhBbnphaGwpKSAlPiUgCiAgdG9wX24oMTApCiAgICAKICBwcmludChyZXN1bHQpCiAgICAKICByZXN1bHQgJT4lICAgCiAgZ2dwbG90KGFlcyh4ID0gQW56YWhsLCB5ID0gaXRlbSkpICsKICAjIGFycmFuZ2UgZGVzYwogIGdlb21fY29sKGFscGhhID0gMC41LCBjb2xvciA9ICJibGFjayIsIGZpbGwgPSAibGltZWdyZWVuIikgKwogIGxhYnModGl0bGUgPSAiVG9wIDEwIEZpbG1lIGRpZSBhbSBow6R1Zmlnc3RlbiBpbiBkZXIgTmFjaGJhcnNjaGFmdCBhbmRlcmUgRmlsbWUgYXVmdGF1Y2hlbiIsCiAgICAgICB4ID0gIkFuemFobCBGaWxtIGFscyBOYWNoYmFyIiwgCiAgICAgICB5ID0gIkZpbG1lIiwKICAgICAgIHN1YnRpdGxlID0gcGFzdGUoInJpYmNmIiwgbl9yZWR1YykpCn0KICAKdG9wXzEwX2l0ZW1fc2ltKHJpYmNmX3NpbV9kZl8xLCAxKQp0b3BfMTBfaXRlbV9zaW0ocmliY2Zfc2ltX2RmXzIsIDIpCgpgYGAKCkbDvHIgamVkZW4gZGVyIGJlaWRlbiBEYXRlbnPDpHR6ZSBoYWJlbiB3aXIgZWluZW4gRGF0YWZyYW1lIHVuZCBQbG90IG1pdCBkZW4gRmlsbWVuLCBkaWUgYW0gaMOkdWZpZ3N0ZW4gaW4gZGVyIENvc2luZS3DhGhubGljaGtlaXRzbWF0cml4IGF1ZnRhdWNoZW4sIGVyc3RlbGx0LiBCZWkgZGVuIHRvcCAxMCBGaWxtZW4gc2luZCBrZWluZSBHZW1laW5zYW1rZWl0ZW4gZXJzaWNodGxpY2guIERpZSBBbnphaGwgZGVyIFZvcmtvbW1lbiBpc3QgYWJlciDDpGhubGljaCwgZGVyIGjDtmNoc3RlIFdlcnQgaXN0IHp3aXNjaGVuIDE1MCB1bmQgMTYwIFZvcmtvbW1lbi4KCiMgNCBJbXBsZW1lbnRpZXJ1bmcgw4RobmxpY2hrZWl0c21hdHJpeApBdWZnYWJlIDQgKERJWSk6IEltcGxlbWVudGllcmUgZWluZSBGdW5rdGlvbiB6dXIgZWZmaXppZW50ZW4KQmVyZWNobnVuZyB2b24gc3BhcnNlbiDDhGhubGljaGtlaXRzbWF0cml6ZW4gZsO8ciBJQkNGIFJTIHVuZAphbmFseXNpZXJlIGRpZSBSZXN1bHRhdGUgZsO8ciAxMDAgenVmw6RsbGlnIGdld8OkaGx0ZSBGaWxtZS4KCiMjIDQuMSBJbXBsZW1lbnRpZXJlIGVpbmUgRnVua3Rpb24sIHVtIChhKSBmw7xyIG9yZGluYWxlIFJhdGluZ3MgZWZmaXppZW50CmRpZSBDb3NpbmUgU2ltaWxhcml0eSB1bmQgKGIpIGbDvHIgYmluw6RyZSBSYXRpbmdzIGVmZml6aWVudCBkaWUgSmFjY2FyZApTaW1pbGFyaXR5IHp1IGJlcmVjaG5lbiwKYGBge3J9Cm51bWJlcl91c2VyIDwtIDEwMApudW1iZXJfaXRlbSA8LSAxMDAKYGBgCgpEaWVzZSBWYXJpYWJsZW4gaGFiZW4gd2lyIGbDvHIgZGllIEVudHdpY2tsdW5nIGRlciBGdW5rdGlvbmVuIHZlcndlbmRldC4gV2lyIGtvbm50ZSBkYW1pdCBlaW5mYWNoIGtsZWluZXJlLCB6LkIuIDUsIERhdGVuc8OkdHplIHNsaWNlbi4KCiMjIyA0LjEuMSBDb3NpbmUgU2ltaWxhcml0eSAKYGBge3J9CmdldF9jb3NzaW1fNCA8LSBmdW5jdGlvbihSYXRpbmdNYXRyaXgsIG5fdXNlciwgbl9pdGVtKXsKIAogIHNsaWNlZF9tYXRyaXggPC0gZ2V0UmF0aW5nTWF0cml4KFJhdGluZ01hdHJpeFsxOm5fdXNlciwgMTpuX2l0ZW1dKQogIAogIHNsaWNlZF9tYXRyaXhfdCA8LSB0KHNsaWNlZF9tYXRyaXgpCiAgCiAgdGVtcF9zaW0gPC0gc2xpY2VkX21hdHJpeF90IC8gc3FydChyb3dTdW1zKHNsaWNlZF9tYXRyaXhfdCAqKiAyKSkKCiAgY29zc2ltX21hdHJpeCA8LSB0ZW1wX3NpbSAlKiUgdCh0ZW1wX3NpbSkKCiAgY29zc2ltX21hdHJpeAp9CmBgYAoKYGBge3J9CnJlc3VsdF9jb3NzaW1fNCA8LSBnZXRfY29zc2ltXzQoTW92aWVMZW5zZSwgbnVtYmVyX3VzZXIsIG51bWJlcl9pdGVtKQpyZXN1bHRfY29zc2ltXzRbMToyMCwxOjIwXQpgYGAKCk1pdCBkZXIgZXJzdGVsbHRlbiBGdW5rdGlvbiBoYWJlbiB3aXIgZsO8ciBkZW4gZ2VzYW10ZW4gTW92aWVMZW5zZSBEYXRlbnNhdHogZGllIENvc2luZSBTaW1pbGFyaXR5IE1hdHJpeCBiZXJlY2huZXQuIFVtIGRhcyBSZXN1bHRhdCBsZXNiYXIgZGFyenVzdGVsbGVuLCB6ZWlnZW4gd2lyIGhpZXIgbnVyIGRpZSBlcnN0ZW4gZsO8bmYgSXRlbS4gQmVpIGRlciBBbmFseXNlIGRlciBlcnN0ZW4gMjAgSXRlbXMgd3VyZGUgZXJzaWNodGxpY2gsIGRhc3MgZGllIFdlcnRlIHp3aXNjaGVuIDAgdW5kIDEgbGllZ2VuLiBOZWdhdGl2ZSBTaW1pbGFyaXRpZXMgc2luZCBuaWNodCBlcnNpY2h0bGljaC4gV2llIG1pdCBkaXIgYmVzcHJvY2hlbiB1bmQgaGVyZ2VsZWl0ZXQsIGlzdCBkYXMgYWJlciB2ZXJzdMOkbmRsaWNoLCBkYSBhdWZncnVuZCBkZXIgbmljaHQtbmVnYXRpdmVuIFJhdGluZ3MgZGVyIG1heGltYWxlIFdpbmtlbCA5MMKwIGJldHLDpGd0LiBIw6R0dGVuIHdpciBtaXQgbm9ybWllcnRlbiBSYXRpbmdzIGdlYXJiZWl0ZXQsIHfDpHJlbiBhdWNoIG5lZ2F0aXZlIFdlcnRlIGF1ZmdldHJldGVuLgoKIyMjIDQuMS4yIEphY2NhcmQgU2ltaWxhcml0eQpgYGB7cn0KZ2V0X2phY2NhcmRzaW1fNCA8LSBmdW5jdGlvbihSYXRpbmdNYXRyaXgsIG5fdXNlciwgbl9pdGVtKXsKICAKICBzbGljZWRfbWF0cml4X2JpbiA8LSBhcyhiaW5hcml6ZShSYXRpbmdNYXRyaXhbMTpuX3VzZXIsIDE6bl9pdGVtXSwgbWluUmF0aW5nPTQpLCAibWF0cml4IikKICAKICBzbGljZWRfbWF0cml4X2Jpbl90IDwtIHQoc2xpY2VkX21hdHJpeF9iaW4pCiAgCiAgbWF0cml4X2NvcnNzcHJvZCA8LSB0Y3Jvc3Nwcm9kKHNsaWNlZF9tYXRyaXhfYmluX3QpCiAgCiAgaW0gPC0gd2hpY2gobWF0cml4X2NvcnNzcHJvZCA+IDAsIGFyci5pbmQ9VFJVRSkKICBiIDwtIHJvd1N1bXMoc2xpY2VkX21hdHJpeF9iaW5fdCkKICBBaW0gPC0gbWF0cml4X2NvcnNzcHJvZFtpbV0KICAKICBKID0gc3BhcnNlTWF0cml4KAogICAgICAgICAgICBpID0gaW1bLDFdLAogICAgICAgICAgICBqID0gaW1bLDJdLAogICAgICAgICAgICB4ID0gQWltIC8gKGJbaW1bLDFdXSArIGJbaW1bLDJdXSAtIEFpbSksCiAgICAgICAgICAgIGRpbXMgPSBkaW0obWF0cml4X2NvcnNzcHJvZCkKICAgICAgKQogIAogIEogPC0gZGF0YS5tYXRyaXgoSikKICAKICBKCn0KCmBgYAoKYGBge3J9CmphY2NhcmRzaW1fNCA8LSBnZXRfamFjY2FyZHNpbV80KE1vdmllTGVuc2UsIG51bWJlcl91c2VyLCBudW1iZXJfaXRlbSkKamFjY2FyZHNpbV80WzE6NSwgMTo1XQpgYGAKCkJlaSBkZXIgSmFjY2FyZCBTaW1pbGFyaXR5IHNpbmQgd2llZGVydW0gbnVyIHBvc2l0aXZlIFdlcnRlIGVyc2ljaHRsaWNoLiBFaW5lIEF1c3dlcnR1bmcgZGllc2VyIGdlcGxvdHRldGVuIFdlcnRlIGVyZ2lidCwgZGFzcyBzZWhyIHdlbmlnZSBXZXJ0ZSDDvGJlciAwLjMgbGllZ2VuLiBEaWUgw4RobmxpY2hrZWl0IGRpZXNlciBlcnN0ZW4gMTAgRmlsbWUgZ2VnZW7DvGJlciBkZW4gZXJzdGVuIDEwMCBhbmRlcmVuIEl0ZW1zIGlzdCBhbHNvIGVoZXIgZ2VyaW5nLgoKIyMgNC4yIFZlcmdsZWljaGUgZGVpbmUgSW1wbGVtZW50aWVydW5nIGRlciBDb3NpbmUtYmFzaWVydGVuCsOEaG5saWNoa2VpdHNtYXRyaXggZsO8ciBvcmRpbmFsZSBSYXRpbmdzIG1pdCBkZXIgdmlhIHJlY29tbWVuZGVybGFiCnVuZCBlaW5lbSBhbmRlcmVuIFItUGFrZXQgZXJ6ZXVndGVuIMOEaG5saWNoa2VpdHNtYXRyaXgsCgojIyMgNC4yLjEgVmVyZ2xlaWNoIENvc2luZSBTaW1pbGlhcnR5IG1pdCBSZWNvbW1lbmRlcmxhYgpgYGB7cn0KI3JlY29tX3NpbWNvc2luXzQgPC0gYXMubWF0cml4KHNpbWlsYXJpdHkobm9ybWFsaXplKE1vdmllTGVuc2VbMTpudW1iZXJfdXNlciwgMTpudW1iZXJfaXRlbV0pLCB3aGljaCA9ICJpdGVtcyIsIG1ldGhvZCA9ICJjb3NpbmUiKSkKcmVjb21fc2ltY29zaW5fNCA8LSBhcy5tYXRyaXgoc2ltaWxhcml0eShNb3ZpZUxlbnNlWzE6bnVtYmVyX3VzZXIsIDE6bnVtYmVyX2l0ZW1dLCB3aGljaCA9ICJpdGVtcyIsIG1ldGhvZCA9ICJjb3NpbmUiKSkKCnJlY29tX3NpbWNvc2luXzRbMTo1LDE6NV0KYGBgCgpgYGB7cn0KcmVzdWx0X2Nvc3NpbV80X3NjYWxlZCA8LSAxIC8gMiAqIChyZXN1bHRfY29zc2ltXzQgKyAxKQoKcmVzdWx0X2Nvc3NpbV80X3NjYWxlZFsxOjUsMTo1XQpgYGAKRsO8ciBkZW4gVmVyZ2xlaWNoIHVuc2VyZXIgSW1wbGVtZW50YXRpb24gbWl0IGRlciB2b24gUmVjb21tZW5kZXJsYWIgaGFiZW4gd2lyIGZlc3RnZXN0ZWxsdCwgZGFzcyBSZWNvbW1lbmRlcmxhYiBkaWUgUmVzdWx0YXRlIGF1ZiBXZXJ0ZSB6d2lzY2hlbiAwIHVuZCAxIG5vcm1pZXJ0LiBXaXIgaGFiZW4gZGllc2UgTm9ybWllcnVuZyBhdWNoIGF1ZiB1bnNlciBSZXN1bHRhdCBkdXJjaGdlZsO8aHJ0LiBPYmVuIGlzdCBkYXMgUmVzdWx0YXQgdm9uIFJlY29tbWVuZGVybGFiIGbDvHIgZGllIGVyc3RlbiBmw7xuZiBGaWxtZSBlcnNpY2h0bGljaCwgZGFydW50ZXIgdW5zZXJlLCBub3JtaWVydGUgSW1wbGVtZW50YXRpb24uIEVzIGzDpHNzdCBzaWNoIGZlc3RzdGVsbGVuLCBkYXNzIGRpZSBXZXJ0ZSB2b24gUmVjb21tZW5kZXJsYWIgc2VociBob2NoLCBpbiBkZXIgTsOkaGUgdm9uIDEgbGllZ2VuLiBVbnNlcmUgaGluZ2VnZW4gcmVpY2hlbiBldHdhIHZvbiAwLjUgYmlzIDEuIFdpciBrb25udGVuIGRpZSBVcnNhY2hlIGRlciBEaWZmZXJlbnogbmljaHQgZmVzdHN0ZWxsZW4sIGVzIHfDpHJlIGFiZXIgZWluIFp1ZmFsbCwgd2VubiBkaWUgw4RobmxpY2hrZWl0ZW4gd2lya2xpY2ggc28gaG9jaCB3w6RyZW4uIFVuc2VyZSBCYW5kYnJlaXRlLCB2b24gMC41IChyZXNwLiB1bm5vcm1pZXJ0IDApIGJpcyAxIGlzdCBkZXNoYWxiIHJlYWxpc3Rpc2NoZXIuCgojIyMgNC4yLjIgVmVyZ2xlaWNoIENvc2luZSBTaW1pbGlhcml0eSBtaXQgYW5kZXJlbSBSLVBha2V0CmBgYHtyfQpsaWJyYXJ5KGxzYSkKCnJlY19zaW1NYXQgPC0gc2ltaWxhcml0eShNb3ZpZUxlbnNlQ29tcGFjdF8xWywxOjVdLCB3aGljaCA9ICJpdGVtcyIpCnJlY19zaW1NYXQKCnJlc3VsdF9jb3NzaW1fNF9zY2FsZWRbMTo1LDE6NV0KYGBgCgpXaXIga29ubnRlbiBrZWluZW4gZGlyZWt0ZW4gVmVyZ2xlaWNoIG1pdCBkZXIgSW1wbGVtZW50YXRpb24gdm9uIExTQSBtYWNoZW4sIHdlaWwgc2llIGRpZSBJdGVtcyBhbmRlcnMgc29ydGllcmVuIHVuZCB3aXIgZGVzaGFsYiBrZWluZSDDnGJlcmVpbnN0aW1tZW5kZSBJdGVtcyBlcmhhbHRlbiBoYWJlbi4gQXVjaCB3ZW5uIHdpciBkaWUgZXJzdGVuIDEwIEl0ZW1zIGFuZ2V6ZWlndCBoYWJlbiwga29ubnRlbiB3aXIga2VpbmUgw5xiZXJlaW5zdGltbXVuZyB2b24gSXRlbXMgZmluZGVuLiBXYXMgYWJlciBhdWZmw6RsbHQgaXN0LCBkYXNzIExTQSBhdWNoIHNlaHIgaG9oZSBTaW1pbGFyaXRpZXMsIGltIEJlcmVpY2ggdm9uIDEsIGJlcmVjaG5ldCBoYXQuCgojIyA0LjMgVmVyZ2xlaWNoZSBkZWluZSBtaXR0ZWxzIENvc2luZSBTaW1pbGFyaXR5IGVyemV1Z3RlbiDDhGhubGljaGtlaXRzbWF0cml4IGbDvHIgb3JkaW5hbGUgUmF0aW5ncyBtaXQgZGVyIEphY2NhcmQtYmFzaWVydGVuIMOEaG5saWNoa2VpdHNtYXRyaXggZsO8ciBiaW7DpHJlIFJhdGluZ3MuCmBgYHtyfQpwcmludCgiQ29zaW5lIFNpbWlsYXJpdHkiKQpyZXN1bHRfY29zc2ltXzRbMTo1LDE6NV0KCnByaW50KCJqYWNjYXJkLWJhc2llcnQiKQpqYWNjYXJkc2ltXzRbMTo1LCAxOjVdCmBgYAoKWndpc2NoZW4gU2ltaWxhcml0eSBiYXNpZXJlbmQgYXVmIENvc2luZSB1bmQgSmFjY2FyZCBzaW5kIGRldXRsaWNoZSBVbnRlcnNjaGllZGUgZXJzaWNodGxpY2guIEJlaSBDb3NpbmUgYmV0cmFnZW4gZGllIG1laXN0ZW4gV2VydGUgendpc2NoZW4gMC4xNSB1bmQgMC40MC4gSW0gR2VnZW5zYXR6IGJldHJhZ2UgZGllIG1laXN0ZW4gV2VydGUgYmVpIEphY2NhcmQgdW0gb2RlciBkZW4gV2VydCBOdWxsLiBEYXMgZMO8cmZ0ZSBkYXJhbiBsaWVnZW4sIGRhc3MgYXVmZ3J1bmQgZGVyIGJpbsOkcmVuIENvZGllcnVuZyBkZXIgUmF0aW5ncywgd2VuaWdlciDDnGJlcmVpbnN0aW1tdW5nZW4gYmVzdGVoZW4uCgojIDUgQW5hbHlzZSBUb3AtTiBMaXN0ZW4gLSBJQkNGIHZzIFVCQ0YKQXVmZ2FiZSA1OiBWZXJnbGVpY2hlIHVuZCBkaXNrdXRpZXJlIFRvcC1OIEVtcGZlaGx1bmdlbiB2b24gSUJDRgp1bmQgVUJDRiBNb2RlbGxlbiBtaXQgMzAgTmFjaGJhcm4gdW5kIENvc2luZSBTaW1pbGFyaXR5IGbDvHIgZGVuCnJlZHV6aWVydGVuIERhdGVuc2F0ei4KIyMgNS4xIEJlcmVjaG5lIFRvcC0xNSBFbXBmZWhsdW5nZW4gZsO8ciBUZXN0a3VuZGVuIG1pdCBJQkNGIHVuZCBVQkNGIAojIyMgNS4xLjEgcmliY2YgJiBydWJjZiBNb2RlbGwgdHJhaW5pZXJlbgpgYGB7cn0KcmliY2ZfMSA8LSBSZWNvbW1lbmRlcih0cmFpbmluZ18xLCAiSUJDRiIsIHBhcmFtPWxpc3Qoaz0gMzAsIG1ldGhvZCA9ICJjb3NpbmUiKSkKcmliY2ZfMQoKcnViY2ZfMSA8LSBSZWNvbW1lbmRlcih0cmFpbmluZ18xLCAiVUJDRiIsIHBhcmFtPWxpc3Qobm49IDMwLCBtZXRob2QgPSAiY29zaW5lIikpCnJ1YmNmXzEKYGBgCgpgYGB7cn0KcmliY2ZfMiA8LSBSZWNvbW1lbmRlcih0cmFpbmluZ18yLCAiSUJDRiIsIHBhcmFtPWxpc3Qoaz0gMzAsIG1ldGhvZCA9ICJjb3NpbmUiKSkKcmliY2ZfMgoKcnViY2ZfMiA8LSBSZWNvbW1lbmRlcih0cmFpbmluZ18yLCAiVUJDRiIsIHBhcmFtPWxpc3Qobm49IDMwLCBtZXRob2QgPSAiY29zaW5lIikpCnJ1YmNmXzIKYGBgCkVzIHd1cmRlbiBmw7xyIGJlaWRlIHJlZHV6aWVydGVuIERhdGVuc8OkdHplIGpld2VpbHMgZWluIGliY2YgdW5kIHViY2YgUmVjb21tZW5kZXIgZXJzdGVsbHQuIAoKIyMjIDUuMS4yIE1vZGVsIFByZWRpY2l0aW9ucyBlcnN0ZWxsZW4KYGBge3J9CnJpYmNmdG9wTkxpc3RfMSA8LSBwcmVkaWN0KHJpYmNmXzEsIHRlc3RfMSwgbj0xNSkKYGBgCgoKYGBge3J9CnJpYmNmdG9wTkxpc3RfMQoKcnViY2Z0b3BOTGlzdF8xIDwtIHByZWRpY3QocnViY2ZfMSwgdGVzdF8xLCBuPTE1KQpydWJjZnRvcE5MaXN0XzEKCnJpYmNmdG9wTkxpc3RfMiA8LSBwcmVkaWN0KHJpYmNmXzIsIHRlc3RfMiwgbj0xNSkKcmliY2Z0b3BOTGlzdF8yCgpydWJjZnRvcE5MaXN0XzIgPC0gcHJlZGljdChydWJjZl8yLCB0ZXN0XzIsIG49MTUpCnJ1YmNmdG9wTkxpc3RfMgpgYGAKCk51biB3dXJkZW4gZsO8ciBiZWlkZSBEYXRlbnPDpHR6ZSBQcmVkaWN0aW9ucyBtaXQgbiA9IDE1IHVuZCBmw7xyIDgwIFVzZXIgYmVyZWNobmV0LgoKIyMjIDUuMS4zIEF1c2dhYmUgdm9uIGVpbmVyIFByZWRpY3Rpb24KYGBge3J9CiMgYXVzZ2FiZSB2b24gZWluZW0gb3V0cHV0CmFzKHJpYmNmdG9wTkxpc3RfMSwgImxpc3QiKVsxOjVdCmBgYAoKRGllcyBpc3QgZWluZSDDnGJlcnNpY2h0IGRlciBFbXBmZWhsdW5nZW4gZsO8ciBkaWUgZXJzdGVuIDUgVXNlci4gV2llIGVyZm9yZGVydCwgd3VyZGVuIGpld2VpbHMgMTUgRW1wZmVobHVuZ2VuIGdlbmVyaWVydC4gQXVmIGRlbiBlcnN0ZW4gQmxpY2sgd2VyZGVuIHZpZWxlIHVudGVyc2NoaWVkbGljaGVuIEZpbG1lIGVtcGZvaGxlbi4KCiMjIDUuMiBWZXJnbGVpY2hlIGRpZSBUb3AtMTUgRW1wZmVobHVuZ2VuIHVuZCBkZXJlbiBWZXJ0ZWlsdW5nIHVuZCBkaXNrdXRpZXJlIEdlbWVpbnNhbWtlaXRlbiB1bmQgVW50ZXJzY2hpZWRlIHp3aXNjaGVuIElCQ0YgdW5kIFVCQ0YgZsO8ciBhbGxlIFRlc3RrdW5kZW4uCgpgYGB7cn0KIyBkZiBmdW5rdGlvbiBlcnN0ZWxsZW4KdG9wTl9kZiA8LSBmdW5jdGlvbih0b3BOTGlzdCl7CiAgY291bnRzIDwtIHRhYmxlKHVubGlzdChhcy5hcnJheShhcyh0b3BOTGlzdCwgImxpc3QiKSkpKQogIGRmIDwtIGRhdGEuZnJhbWUoTW92aWUgPSBuYW1lcyhjb3VudHMpLCBDb3VudCA9IHVubmFtZShjb3VudHMpKSAlPiUKICAgIHNlbGVjdCgiTW92aWUiLCAiQ291bnQuRnJlcSIpICU+JQogICAgcmVuYW1lKCJDb3VudCIgPSAiQ291bnQuRnJlcSIpICU+JQogICAgYXJyYW5nZShkZXNjKENvdW50KSkgIAogIGRmCn0KCiMgYWxsZSBkZnMgZXJzdGVsbGVuCnJpYmNmdG9wTl9kZl8xIDwtIHRvcE5fZGYocmliY2Z0b3BOTGlzdF8xKQpyaWJjZnRvcE5fZGZfMQpyaWJjZnRvcE5fZGZfMiA8LSB0b3BOX2RmKHJpYmNmdG9wTkxpc3RfMikKcmliY2Z0b3BOX2RmXzIKCnJ1YmNmdG9wTl9kZl8xIDwtIHRvcE5fZGYocnViY2Z0b3BOTGlzdF8xKQpydWJjZnRvcE5fZGZfMQpydWJjZnRvcE5fZGZfMiA8LSB0b3BOX2RmKHJ1YmNmdG9wTkxpc3RfMikKcnViY2Z0b3BOX2RmXzIKCmBgYAoKRGllIGVyc3RlbiBiZWlkZW4gVGFiZWxsZW4gc3RlbGxlbiBkaWUgRW1wZmVobHVuZ2VuIHVuZCBkZXJlbiBBbnphaGwgYmFzaWVyZW5kIGF1ZiBJQkNGIGbDvHIgZGllIGJlaWRlbiBEYXRlbnPDpHR6ZSBkYXIuIEluIGRlbiBUb3AgMTAgRW1wZmVobHVuZ2VuIHNpbmQgc2VociB1bnRlcnNjaGllZGxpY2hlIEVtcGZlaGx1bmdlbiwgZXMgZ2lidCBrYXVtIMOcYmVyc2NobmVpZHVuZ2VuLiBGw7xyIGRlbiBlcnN0ZW4gRGF0ZW5zYXR6IHdpcmQgZWluIEZpbG0gbWF4aW1hbCAxMSBtYWwsIGltIHp3ZWl0ZW4gbWF4aW1hbCAxNSBtYWwgZW1wZm9obGVuLiBCZWltIGVyc3RlbiBEYXRlbnNhdHogd2VyZGVuIGluc2dlc2FtdCA0ODcgRmlsbWUgdW5kIGJlaW0gendlaXRlbiA0MDcgZW1wZm9obGVuLgoKRGllIGxldHp0ZW4gYmVpZGVuIFRhYmVsbGVuIHN0ZWxsZW4gZGllIEVtcGZlaGx1bmdlbiBiYXNpZXJlbmQgYXVmIFVCQ0YgZsO8ciBkaWUgYmVpZGVuIERhdGVuc8OkdHplIGRhci4gRGllIFRvcCAxMCBGaWxtZSBzaW5kIHdpZWRlciBzZWhyIHVudGVyc2NoaWVkbGljaC4gR3Jvc3NlIFVudGVyc2NoaWVkZSBnaWJ0IGVzIGF1Y2ggYmVpIGRlciBBbnphaGwgVm9ya29tbWVuIGRlciBUb3AgRmlsbWUuIEbDvHIgZGVuIGVyc3RlbiBEYXRlbnNhdHogd2VyZGVuIHNpZSBiaXMgenUgMzAgbWFsIGVtcGZvaGxlbiwgd8OkaHJlbmQgZXMgYmVpbSB6d2VpdGVuIG1heGltYWwgMTQgbWFsIHdhci4gQXVjaCBsaWVndCBkaWUgQW56YWhsIEVtcGZlaGx1bmdlbiBtaXQgMzAxIHZzIDM5MiB3ZWl0IGF1c2VpbmFuZGVyLgoKRsO8ciB3ZWl0ZXJlIEluZm9ybWF0aW9uZW4gdmlzdWFsaXNpZXJlbiB3aXIgbnVuIGF1Y2ggZGllIFRvcCBFbXBmZWhsdW5nZW4uCgojIyMgNS4yLjEgVmVydGVpbHVuZ2VuIHZpc3VhbGlzaWVyZW4KYGBge3IsIGZpZy5oZWlnaHQ9OCwgZmlnLndpZHRoPTE1fQojIGZ1bmt0aW9uIHp1ciBWaXN1YWxpc2llcnVuZwp0b3AxNV9kZl92aXN1YWxpemUgPC0gZnVuY3Rpb24odG9wTkxpc3QsIHN1YnRpdGxlKXsKICB0b3BOTGlzdCAlPiUgaGVhZCgxNSkgJT4lIAogICAgZ2dwbG90KGFlcyh4ID0gcmVvcmRlcihNb3ZpZSwgQ291bnQpLCB5ID0gQ291bnQpKSArCiAgICBnZW9tX2JhcihzdGF0ID0gImlkZW50aXR5IiwgZmlsbCA9ICJsaW1lZ3JlZW4iLCBhbHBoYSA9IDAuNSwgY29sb3IgPSAiYmxhY2siKSArCiAgICBjb29yZF9mbGlwKCkgKwogICAgbGFicyh4ID0gIk1vdmllIiwgCiAgICAgICAgIHkgPSAiQW56YWhsIiwgCiAgICAgICAgIHRpdGxlID0gIlRvcC0xNSBFbXBmZWhsdW5nZW4iLAogICAgICAgICBzdWJ0aXRsZSA9IHN1YnRpdGxlKQp9CgpncmlkLmFycmFuZ2UodG9wMTVfZGZfdmlzdWFsaXplKHJpYmNmdG9wTl9kZl8xLCAicmliY2YgMSIpLAogICAgICAgICAgICAgdG9wMTVfZGZfdmlzdWFsaXplKHJ1YmNmdG9wTl9kZl8xLCAicnViY2YgMSIpLAogICAgICAgICAgICAgbmNvbCA9IDIpCgoKZ3JpZC5hcnJhbmdlKHRvcDE1X2RmX3Zpc3VhbGl6ZShyaWJjZnRvcE5fZGZfMiwgInJpYmNmIDIiKSwKICAgICAgICAgICAgIHRvcDE1X2RmX3Zpc3VhbGl6ZShydWJjZnRvcE5fZGZfMiwgInJ1YmNmIDIiKSwKICAgICAgICAgICAgIG5jb2wgPSAyKQoKYGBgCkRhbmsgZGVyIGxpYnJhcnkgZ3JpZEV4dHJhIGvDtm5uZW4gd2lyIGRpZSBiZWlkZW4gRGF0ZW5zw6R0emUgbmViZW5laW5hbmRlciBkYXJzdGVsbGVuLiBFcnNpY2h0bGljaCB3aXJkLCB3aWUgc2NobmVsbCBkaWUgQW56YWhsIEVtcGZlaGx1bmdlbiBwcm8gRmlsbSBhYm5pbW10LiBJbiBkZXIgZXJzdGVuIExhc2NoZSwgSUJDRiwgc2llaHQgbWFuLCBkYXNzIGRpZSBBbnphaGwgbGluZWFyIGFibmltbXQsIG5hY2hkZW0gZGllIGVyc3RlbiBmw7xuZiBGaWxtZSBnbGVpY2ggaMOkdWZpZyBlbXBmb2hsZW4gd2VyZGVuLiBIaW5nZWdlbiBuZWhtZW4gZGllIEFuemFobCBpbSB6d2VpdGVuIERhdGVuc2F0eiAoR3JhZmlrIHJlY2h0cykgenVlcnN0IHNjaG5lbGwsIGJpcyBldHdhIHp1bSBOaXZlYXUgZGVzIGVyc3RlbiBEYXRlbnNhdHplcywgZGFubiBsaW5lYXIgYWIuIEJlaSBVQkNGLCBpbiBkZXIgendlaXRlbiBMYXNjaGUsIG5pbW10IGRpZSBBbnphaGwgYmVpIGJlaWRlbiBEYXRlbnPDpHR6ZW4gbGluZWFyIGFiLgoKRGllIGVyd8OkaG50ZSBCZWhhdXB0dW5nIOKAnFJlY29tbWVuZGVyIFN5c3RlbWUgbWFjaGVuIGbDvHIgYWxsZSBOdXR6ZXIgZGllIGdsZWljaGVuIEVtcGZlaGx1bmdlbuKAnSBrYW5uIGRhbmsgZGVyIFRhYmVsbGVuIHVuZCBIaXN0b2dyYW1tZSB2ZXJ3b3JmZW4gd2VyZGVuLiBFcyB3ZXJkZW4gdmllbGUgdW50ZXJzY2hpZWRsaWNoZSBGaWxtZSBlbXBmb2hsZW4sIHZpZWxldmllbGUgRmlsbWUgd2VyZGVuIG51ciB3ZW5pZ2VuIFVzZXJuICg8NCkgZW1wZm9obGVuLgoKIyA2IEFuYWx5c2UgVG9wLU4gTGlzdGVuIC0gUmF0aW5ncwpBdWZnYWJlIDY6IFVudGVyc3VjaGUgZGVuIEVpbmZsdXNzIHZvbiBSYXRpbmdzIChvcmRpbmFsZSB2cyBiaW7DpHJlIFJhdGluZ3MpIHVuZCBNb2RlbGx0eXAgKElCQ0YgdnMgVUJDRikgYXVmIFRvcC1OIEVtcGZlaGx1bmdlbiBmw7xyIGRlbiByZWR1emllcnRlbiBEYXRlbnNhdHouIFZlcmdsZWljaGUgZGVuIEFudGVpbCDDvGJlcmVpbnN0aW1tZW5kZXIgRW1wZmVobHVuZ2VuIGRlciBUb3AtMTUgTGlzdGUgZsO8cgojIyA2LjEgSUJDRiB2cyBVQkNGLCBiZWlkZSBtaXQgb3JkaW5hbGVtIFJhdGluZyB1bmQgQ29zaW5lIFNpbWlsYXJpdHkgZsO8ciBhbGxlIFRlc3RrdW5kZW4sCmBgYHtyfQpjb21wYXJlX2liY2ZfdWJjZiA8LSBmdW5jdGlvbihpYmNmLCB1YmNmKSB7CiAgcHJpbnQocGFzdGUoIkFuemFobCBJQkNGOiIsIG5yb3coaWJjZikpKQogIHByaW50KHBhc3RlKCJBbnphaGwgVUJDRjoiLCBucm93KHViY2YpKSkKCiAgSW50ZXJzZWN0b3JkUmF0Q29zaW5lIDwtIGludGVyc2VjdChpYmNmJE1vdmllLCB1YmNmJE1vdmllKQoKICBwcmludChwYXN0ZSgiQW56YWhsIGdlbWVpbnNhbWUgRW1wZmVobHVuZ2VuOiIsIGxlbmd0aChJbnRlcnNlY3RvcmRSYXRDb3NpbmUpKSkKICBwcmludChwYXN0ZSgiQW50ZWlsIElCQ0Y6IiwgbGVuZ3RoKEludGVyc2VjdG9yZFJhdENvc2luZSkgLyBucm93KGliY2YpICogMTAwKSkKICBwcmludChwYXN0ZSgiQW50ZWlsIFVCQ0Y6IiwgbGVuZ3RoKEludGVyc2VjdG9yZFJhdENvc2luZSkgLyBucm93KHViY2YpICogMTAwKSkKfQoKcHJpbnQoIkVyc3RlIERhdGVucmVkdWt0aW9uIikKY29tcGFyZV9pYmNmX3ViY2YocmliY2Z0b3BOX2RmXzEsIHJ1YmNmdG9wTl9kZl8xKQpwcmludCgiWndlaXRlIERhdGVucmVkdWt0aW9uIikKY29tcGFyZV9pYmNmX3ViY2YocmliY2Z0b3BOX2RmXzIsIHJ1YmNmdG9wTl9kZl8yKQpgYGAKCkVyc3RlIERhdGVucmVkdWt0aW9uOiBGw7xyIElCQ0Ygd2VyZGVuIDQ4NyB1bmQgVUJDRiAzMDEgRmlsbWUgZW1wZm9obGVuLCBkYWJlaSBnaWJ0IGVzIGVpbmUgw5xiZXJlaW5zdGltbXVuZyB2b24gMjMxIEZpbG1lbi4KRGFzIGVudHNwcmVjaGVuIGJlaSBJQkNGIDQ3LjUlIHVuZCBiZWkgVUJDRiA3Ni43JS4KWndlaXRlIERhdGVucmVkdWt0aW9uOiBGw7xyIElCQ0Ygd2VyZGVuIDQwNyB1bmQgVUJDRiAzOTIgRmlsbWUgZW1wZm9obGVuLCBkYWJlaSBnaWJ0IGVzIGVpbmUgw5xiZXJlaW5zdGltbXVuZyB2b24gMjI2IEZpbG1lbi4KRGFzIGVudHNwcmVjaGVuIGJlaSBJQkNGIDU1JSB1bmQgYmVpIFVCQ0YgNTclLgoKSW5zZ2VzYW10IGdlbmVyaWVyZW4gYWxzbyBiZWlkZSBNZXRob2RlbiDDpGhubGljaGUgRW1wZmVobHVuZ2VuLCBydW5kIGRpZSBIw6RsZnRlIGJpcyAzLzQgZGVyIEVtcGZlaGx1bmdlbiBnZW5lcmllcnQgYXVjaCBkaWUgYW5kZXJlIE1ldGhvZGUuIEF1ZmbDpGxsaWcgaXN0IGhpbmdlZ2VuIGJlaW0gZXJzdGVuIERhdGVuc2F0eiwgZGFzcyBJQkNGIHZpZWwgbWVociBGaWxtZSBlbXBmaWVobHQsIHfDpGhyZW5kIGVzIGJlaW0gendlaXRlbiBldHdhIGdsZWljaCB2aWVsIHNpbmQuCgpCZWltIHp3ZWl0ZW4gRGF0ZW5zYXR6IGlzdCBhdWNoIGRlciBBbnRlaWwgYW4gR2VtZWluc2Fta2VpdGVuIGpld2VpbHMgYmVpIHJ1bmQgNTUlIHVuZCBkYW1pdCBhdXNnZWdsaWNoZW5lciBhbHMgaW0gZXJzdGVuIERhdGVuc2F0ei4gSWNoIGthbm4gbWlyIHZvcnN0ZWxsZW4sIGRhc3MgZGFzIGRhcmFuIGxpZWd0LCBkYXNzIGJlaW0gendlaXRlbiBEYXRlbnNhdHogZGllIFNwYXJzaXR5IGRlciBNYXRyaXggaMO2aGVyIGlzdCB1bmQgZGFtaXQgbWVociBTcGllbHJhdW0gb2ZmZW4gaXN0LgoKIyMgNi4yIElCQ0YgdnMgVUJDRiwgYmVpZGUgbWl0IGJpbsOkcmVtIFJhdGluZyB1bmQgSmFjY2FyZCBTaW1pbGFyaXR5IGbDvHIgYWxsZSBUZXN0a3VuZGVuLApgYGB7cn0KdHJhaW5pbmdfYmluXzEgPC0gYmluYXJpemUodHJhaW5pbmdfMSwgbWluUmF0aW5nID0gNCkKdGVzdF9iaW5fMSA8LSBiaW5hcml6ZSh0ZXN0XzEsIG1pblJhdGluZyA9IDQpCgp0cmFpbmluZ19iaW5fMiA8LSBiaW5hcml6ZSh0cmFpbmluZ18yLCBtaW5SYXRpbmcgPSA0KQp0ZXN0X2Jpbl8yIDwtIGJpbmFyaXplKHRlc3RfMiwgbWluUmF0aW5nID0gNCkKCnJpYmNmX2Jpbl8xIDwtIFJlY29tbWVuZGVyKHRyYWluaW5nX2Jpbl8xLCAiSUJDRiIsIHBhcmFtPWxpc3Qoaz0gMzAsIG1ldGhvZCA9ICJqYWNjYXJkIikpCnJpYmNmX2Jpbl8xCgpydWJjZl9iaW5fMSA8LSBSZWNvbW1lbmRlcih0cmFpbmluZ19iaW5fMSwgIlVCQ0YiLCBwYXJhbT1saXN0KG5uPSAzMCwgbWV0aG9kID0gImphY2NhcmQiKSkKcnViY2ZfYmluXzEKCnJpYmNmX2Jpbl8yIDwtIFJlY29tbWVuZGVyKHRyYWluaW5nX2Jpbl8yLCAiSUJDRiIsIHBhcmFtPWxpc3Qoaz0gMzAsIG1ldGhvZCA9ICJqYWNjYXJkIikpCnJpYmNmX2Jpbl8yCgpydWJjZl9iaW5fMiA8LSBSZWNvbW1lbmRlcih0cmFpbmluZ19iaW5fMiwgIlVCQ0YiLCBwYXJhbT1saXN0KG5uPSAzMCwgbWV0aG9kID0gImphY2NhcmQiKSkKcnViY2ZfYmluXzIKCmBgYAoKYGBge3J9CnJpYmNmdG9wTkxpc3RfYmluXzEgPSBwcmVkaWN0KHJpYmNmX2Jpbl8xLCB0ZXN0X2Jpbl8xLCBuPTE1KQpyaWJjZnRvcE5MaXN0X2Jpbl8xCgpydWJjZnRvcE5MaXN0X2Jpbl8xID0gcHJlZGljdChydWJjZl9iaW5fMSwgdGVzdF9iaW5fMSwgbj0xNSkKcnViY2Z0b3BOTGlzdF9iaW5fMQoKcmliY2Z0b3BOTGlzdF9iaW5fMiA9IHByZWRpY3QocmliY2ZfYmluXzIsIHRlc3RfYmluXzIsIG49MTUpCnJpYmNmdG9wTkxpc3RfYmluXzIKCnJ1YmNmdG9wTkxpc3RfYmluXzIgPSBwcmVkaWN0KHJ1YmNmX2Jpbl8yLCB0ZXN0X2Jpbl8yLCBuPTE1KQpydWJjZnRvcE5MaXN0X2Jpbl8yCmBgYAoKYGBge3J9CnJpYmNmdG9wTl9kZl9iaW5fMSA8LSB0b3BOX2RmKHJpYmNmdG9wTkxpc3RfYmluXzEpCnJpYmNmdG9wTl9kZl9iaW5fMQoKcmliY2Z0b3BOX2RmX2Jpbl8yIDwtIHRvcE5fZGYocmliY2Z0b3BOTGlzdF9iaW5fMikKcmliY2Z0b3BOX2RmX2Jpbl8yCgpydWJjZnRvcE5fZGZfYmluXzEgPC0gdG9wTl9kZihydWJjZnRvcE5MaXN0X2Jpbl8xKQpydWJjZnRvcE5fZGZfYmluXzEKCnJ1YmNmdG9wTl9kZl9iaW5fMiA8LSB0b3BOX2RmKHJ1YmNmdG9wTkxpc3RfYmluXzIpCnJ1YmNmdG9wTl9kZl9iaW5fMgpgYGAKCkRpZXNlIEF1c3dlcnR1bmcgZW50c3ByaWNodCBkZXIsIGRlciB2b3JoZXJpZ2VuIEF1ZmdhYmUsIG51ciBkYXNzIGRpZXNlcyBtYWwgbWl0IGJpbsOkcmVuIFJhdGluZ3MgdW5kIEphY2NhcmQgU2ltaWxhcml0eSBnZWFyYmVpdGV0IHd1cmRlLgpFcyB3aXJkIGF1Y2ggaGllciBlcnNpY2h0bGljaCwgZGFzcyBkaWUgRW1wZmVobHVuZ2VuIHNlaHIgdW50ZXJzY2hpZWRsaWNoIHNpbmQuIEJlaSBJQkNGIChlcnN0ZSB6d2VpIFRhYmVsbGVuKSB3ZXJkZW4gZsO8ciBkZW4gZXJzdGVuIERhdGVuc2F0eiBkaWUgVG9wIEZpbG1lIHNlaHIgdmllbCBow6R1ZmlnZXIgKDM5IG1hbCB2cyAxNiBtYWwpIGVtcGZvaGxlbi4gRGFzIGdsZWljaGUgTXVzdGVyLCB3ZW5uIGFiZXIgc2Nod8OkY2hlciwgaXN0IGJlaSBkZW4gVUJDRiBlcnNpY2h0bGljaC4KCmBgYHtyfQpwcmludCgiRXJzdGUgRGF0ZW5yZWR1a3Rpb24gYmluYWVyIikKY29tcGFyZV9pYmNmX3ViY2YocmliY2Z0b3BOX2RmX2Jpbl8xLCBydWJjZnRvcE5fZGZfYmluXzEpCnByaW50KCJad2VpdGUgRGF0ZW5yZWR1a3Rpb24gYmluYWVyIikKY29tcGFyZV9pYmNmX3ViY2YocmliY2Z0b3BOX2RmX2Jpbl8yLCBydWJjZnRvcE5fZGZfYmluXzIpCmBgYApJbSBHZWdlbnNhdHogenVyIHZvcmhlcmlnZW4gQXVmZ2FiZSB1bmQgZGVuIHp3ZWl0ZW4gRGF0ZW5zYXR6LCBnaWJ0IGVzIGbDvHIgZGVuIGVyc3RlbiBmYXN0IGtlaW5lIGdlbWVpbnNhbWUgRW1wZmVobHVuZ2VuLiBFcyBmw6RsbHQgYXVjaCBhdWYsIGRhc3MgZsO8ciBJQkNGIG51ciA4NyBGaWxtZSBlbXBmb2hsZW4gd2VyZGVuLiBEaWVzZSBBdXN3ZXJ0dW5nIHd1cmRlIG1pdCBtaW5SYXRpbmcgNCBmw7xyIGRpZSBiaW7DpHJlIEtsYXNzaWZpemllcnVuZyBiZXJlY2huZXQuIE1pdCBSYXRpbmcgMyBzaWVodCBkaWVzZXIgU2FjaHZlcmhhbHQgw6RobmxpY2ggYXVzLCBiZWkgbWluUmF0aW5nIDUgaXN0IGRpZSDDnGJlcmVpbnN0aW1tdW5nIGFiZXIgd2llZGVyIGltIG5vcm1hbGVuIEJlcmVpY2guIFdpZXNvIG1pblJhdGluZyAzIHVuZCA0IHNvIHRpZWZlIMOcYmVyZWluc3RpbW11bmdlbiBnZW5lcmllcnQgaGFiZW4sIGvDtm5uZW4gd2lyIG5pY2h0IG5hY2h2b2xsemllaGUuIERhc3MgbWluUmF0aW5nIDUgYWJlciBiZXNzZXJlIFJlc3VsdGF0ZSBnZW5lcmllcnQsIGxpZWd0IGRhcmFuLCBkYXNzIG51biBudXIgbm9jaCB3ZW5pZ2UgSXRlbXMgYWxzIDEga2xhc3NpZml6aWVydCB3ZXJkZW4gdW5kIGRhbWl0IHdlbmlnZXIgRmlsbWUgenVyIEVtcGZlaGx1bmcgenVyIFZlcmbDvGd1bmcgc3RlbGxlbi4KCgojIyA2LjMgVUJDRiBtaXQgb3JkaW5hbGVtIChDb3NpbmUgU2ltaWxhcml0eSkgdnMgVUJDRiBtaXQgYmluw6RyZW0gUmF0aW5nIChKYWNjYXJkIFNpbWlsYXJpdHkpIGbDvHIgYWxsZSBUZXN0a3VuZGVuLgoKYGBge3J9CmNvbXBhcmVfdWJjZiA8LSBmdW5jdGlvbihpYmNmLCB1YmNmKSB7CiAgcHJpbnQocGFzdGUoIkFuemFobCBVQkNGIG9yZDoiLCBucm93KGliY2YpKSkKICBwcmludChwYXN0ZSgiQW56YWhsIFVCQ0YgYmluOiIsIG5yb3codWJjZikpKQoKICBJbnRlcnNlY3RvcmRSYXRDb3NpbmUgPC0gaW50ZXJzZWN0KGliY2YkTW92aWUsIHViY2YkTW92aWUpCgogIHByaW50KHBhc3RlKCJBbnphaGwgZ2VtZWluc2FtZSBFbXBmZWhsdW5nZW46IiwgbGVuZ3RoKEludGVyc2VjdG9yZFJhdENvc2luZSkpKQogIHByaW50KHBhc3RlKCJBbnRlaWwgVUJDRiBvcmQ6IiwgbGVuZ3RoKEludGVyc2VjdG9yZFJhdENvc2luZSkgLyBucm93KGliY2YpICogMTAwKSkKICBwcmludChwYXN0ZSgiQW50ZWlsIFVCQ0YgYmluOiIsIGxlbmd0aChJbnRlcnNlY3RvcmRSYXRDb3NpbmUpIC8gbnJvdyh1YmNmKSAqIDEwMCkpCn0KYGBgCgpFcnN0ZWxsdW5nIGRlciBGdW5rdGlvbiB1bmQgQmVyZWNobnVuZyBkZXMgUmVzdWx0YXRzCgpgYGB7cn0KcHJpbnQoIkVyc3RlIERhdGVucmVkdWt0aW9uIikKY29tcGFyZV91YmNmKHJ1YmNmdG9wTl9kZl8xLCBydWJjZnRvcE5fZGZfYmluXzEpCnByaW50KCJad2VpdGUgRGF0ZW5yZWR1a3Rpb24iKQpjb21wYXJlX3ViY2YocnViY2Z0b3BOX2RmXzIsIHJ1YmNmdG9wTl9kZl9iaW5fMikKYGBgCgpCZWltIFZlcmdsZWljaCB2b24gVUJDRiBtaXQgb3JkaW5hbGVtIHVuZCBiaW7DpHJlbSBSYXRpbmcgd2VyZGVuIHdpZWRlciBtZWhyIMO8YmVyZWluc3RpbW1lbmRlIEZpbG1lIGVtcGZvaGxlbi4gRsO8ciBkZW4gZXJzdGVuIERhdGVuc2F0eiB3ZXJkZW4gMjA3IEZpbG1lIGJlaSBiZWlkZW4gTW9kZWxsZW4gdW5kIGJlaW0gendlaXRlbiBEYXRlbnNhdHogMzAxIMO8YmVyZWluc3RpbW1lbmRlIEZpbG1lIGVtcGZvaGxlbi4gRGEgYmVpIGJlaWRlbiBEYXRlbnPDpHR6ZW4gbWl0IG9yZGluYWxlbSBSYXRpbmcgd2VuaWdlciBFbXBmZWhsdW5nZW4gZ2VuZXJpZXJ0IHdlcmRlbiwgaXN0IGRlciBBbnRlaWwgw5xiZXJlaW5zdGltbXVuZ2VuIGJlaSBvcmRpbmFsZW4gUmF0aW5ncyBlbnRzcHJlY2hlbmQgaMO2aGVyLgoKIyA3IEFuYWx5c2UgVG9wLU4gTGlzdGVuIC0gSUJDRiB2cyBTVkQKQXVmZ2FiZSA3OiBWZXJnbGVpY2hlCk1lbW9yeS1iYXNlZCBJQkNGIHVuZCBNb2RlbGwtYmFzZWQgU1ZEIFJlY29tbWVuZGVycyBiZXrDvGdsaWNoIMOcYmVyc2NobmVpZHVuZyBpaHJlciBUb3AtTiBFbXBmZWhsdW5nZW4gZsO8ciBkaWUgVXNlci1JdGVtIE1hdHJpeCBkZXMgcmVkdXppZXJ0ZW4gRGF0ZW5zYXR6ZXMgKEJhc2lzOiByZWR1emllcnRlciBEYXRlbnNhdHosIElCQ0YgbWl0IDMwIE5hY2hiYXJuIHVuZCBDb3NpbmUgU2ltaWxhcml0eSkuClZlcmdsZWljaGUgd2llIHNpY2ggZGVyIEFudGVpbCDDvGJlcmVpbnN0aW1tZW5kZXIgRW1wZmVobHVuZ2VuIGRlciBUb3AtMTUgTGlzdGUgZsO8ciBJQkNGIHZzIHZlcnNjaGllZGVuZSBTVkQgTW9kZWxsZSB2ZXLDpG5kZXJ0LCB3ZW5uIGRpZSBBbnphaGwgZGVyIFNpbmd1bMOkcndlcnRlIGbDvHIgU1ZEIHZvbiAxMCBhdWYgMjAsIDMwLCA0MCwgNTAgdmVyw6RuZGVydCB3aXJkLgpgYGB7cn0KIyBGdW5rdGlvbiBmdWVyIFNWRCBNb2RlbApnZW5lcmF0ZV9TVkRfdG9wTl9yZWNvbW0gPC0gZnVuY3Rpb24odHJhaW4sIHRlc3QsIHN2ZF92YWx1ZSA9IGtzdmQpewoJcmVjb21fbW9kZWwgPC0gUmVjb21tZW5kZXIodHJhaW4sICJTVkQiLCBwYXJhbT1saXN0KGs9IHN2ZF92YWx1ZSkpCgl0b3Bfbl9yZWNvbSA8LSBwcmVkaWN0KHJlY29tX21vZGVsLCB0ZXN0LCBuPTE1KQogIHRvcF9uX3JlY29tCn0KCiMgRnVua3Rpb24gZnVlciB2ZXJzY2hpZWRlbmUgTgpnZW5lcmF0ZV9TVkRfdG9wTl9saXN0cyA8LSBmdW5jdGlvbih0cmFpbiwgdGVzdCwgTl92YWx1ZXMpIHsKICByc3ZkX3RvcE5fbGlzdHMgPC0gbGlzdCgpCiAgZm9yIChpIGluIDE6bGVuZ3RoKE5fdmFsdWVzKSkgewogICAgTiA8LSBOX3ZhbHVlc1tpXQogICAgbGlzdF9uYW1lIDwtIHBhc3RlMCgicnN2ZCIsIE4sICJ0b3BOTGlzdCIpCiAgICByc3ZkX3RvcE5fbGlzdHNbW2xpc3RfbmFtZV1dIDwtIGdlbmVyYXRlX1NWRF90b3BOX3JlY29tbSh0cmFpbiwgdGVzdCwgTikKICB9CiAgcnN2ZF90b3BOX2xpc3RzCn0KYGBgCgpGdW5rdGlvbiB6dXIgQmVyZWNobnVuZyBkZXMgUmVzdWx0YXRzCgpgYGB7cn0KTl92YWx1ZXMgPC0gYygxMCwgMjAsIDMwLCA0MCwgNTApCnJzdmRfdG9wTl9saXN0c18xIDwtIGdlbmVyYXRlX1NWRF90b3BOX2xpc3RzKHRyYWluaW5nXzEsIHRlc3RfMSwgTl92YWx1ZXMpCnByaW50KCJFcnN0ZXIgRGF0ZW5zYXR6IikKcnN2ZF90b3BOX2xpc3RzXzEKCnJzdmRfdG9wTl9saXN0c18yIDwtIGdlbmVyYXRlX1NWRF90b3BOX2xpc3RzKHRyYWluaW5nXzIsIHRlc3RfMiwgTl92YWx1ZXMpCnByaW50KCJad2VpdGVyIERhdGVuc2F0eiIpCnJzdmRfdG9wTl9saXN0c18yCmBgYAoKCmBgYHtyfQpnZW5lcmF0ZV90b3BOX2RmcyA8LSBmdW5jdGlvbihyc3ZkX3RvcE5fbGlzdHMpIHsKICB0b3BOX2RmcyA8LSBsaXN0KCkKICAKICBmb3IgKGkgaW4gMTpsZW5ndGgocnN2ZF90b3BOX2xpc3RzKSkgewogICAgbGlzdF9uYW1lIDwtIG5hbWVzKHJzdmRfdG9wTl9saXN0cylbaV0KICAgIGRmX25hbWUgPC0gcGFzdGUwKGxpc3RfbmFtZSwgIl9kZiIpCiAgICB0b3BOX2Rmc1tbZGZfbmFtZV1dIDwtIHRvcE5fZGYocnN2ZF90b3BOX2xpc3RzW1tpXV0pCiAgfQogIAogIHRvcE5fZGZzCn0KCnRvcE5fZGZfc3ZkXzEgPC0gZ2VuZXJhdGVfdG9wTl9kZnMocnN2ZF90b3BOX2xpc3RzXzEpCnByaW50KCJFcnN0ZXIgRGF0ZW5zYXR6IikKdG9wTl9kZl9zdmRfMQoKdG9wTl9kZl9zdmRfMiA8LSBnZW5lcmF0ZV90b3BOX2Rmcyhyc3ZkX3RvcE5fbGlzdHNfMikKcHJpbnQoIlp3ZWl0ZXIgRGF0ZW5zYXR6IikKdG9wTl9kZl9zdmRfMgoKCmBgYAoKRGllIGVyc3RlbiBmw7xuZiBUYWJlbGxlbiBzaW5kIGRpZSBSZXN1bHRhdGUgZsO8ciBkZW4gZXJzdGVuIERhdGVuc2F0ei4gRXMgaGFuZGVsdCBzaWNoIGF1ZnN0ZWlnZW5kIHVtIGRpZSBBbnphaGwgU2luZ3Vsw6Ryd2VydGUgdm9uIDEwIGJpcyA1MC4gRGllIGxldHp0ZW4gZsO8bmYgVGFiZWxsZW4gYmVpbmhhbHRlbiBkYXMgZ2xlaWNoZSBSZXN1bHRhdCwgZWluZmFjaCBmw7xyIGRlbiB6d2VpdGVuIERhdGVuc2F0ei4gRGllc2UgQXVzd2VydHVuZyB3aXJkIG5vY2ggbmljaHQgZsO8ciBkaWUgQmVhbnR3b3J0dW5nIGRlciBBdWZnYWJlIHZlcndlbmRldCwgc29uZGVybiBnYWIgdW5zIGVpbmVuIGVyc3RlbiDDnGJlcmJsaWNrIMO8YmVyIGRpZSBSZXN1bHRhdGUKCmBgYHtyfQpjb21wYXJlX2liY2Zfc3ZkIDwtIGZ1bmN0aW9uKHJpYmNmLCBzdmQsIHN2ZF92YWx1ZSkgewogIGludGVyc2VjdCA8LSBpbnRlcnNlY3QocmliY2YkTW92aWUsIHN2ZCRNb3ZpZSkKICBwcmludChwYXN0ZSgiQW56YWhsIGdlbWVpbnNhbWUgRW1wZmVobHVuZ2VuIFNWRCIsIHN2ZF92YWx1ZSwgIjoiLCBsZW5ndGgoaW50ZXJzZWN0KSkpCiAgcHJpbnQocGFzdGUoIkdlbWVpbnNhbWVyIHJlbGF0aXZlciBBbnRlaWwgZsO8ciBBbnphaGwgU2luZ3Vsw6Ryd2VydGUiLCBzdmRfdmFsdWUsICI6IiwgbGVuZ3RoKGludGVyc2VjdCkgLyBucm93KHJpYmNmKSAqIDEwMCkpCn0KCnByaW50KCJFcnN0ZXIgRGF0ZW5zYXR6IikKY29tcGFyZV9pYmNmX3N2ZChyaWJjZnRvcE5fZGZfMSwgdG9wTl9kZl9zdmRfMSRyc3ZkMTB0b3BOTGlzdF9kZiwgMTApCmNvbXBhcmVfaWJjZl9zdmQocmliY2Z0b3BOX2RmXzEsIHRvcE5fZGZfc3ZkXzEkcnN2ZDIwdG9wTkxpc3RfZGYsIDIwKQpjb21wYXJlX2liY2Zfc3ZkKHJpYmNmdG9wTl9kZl8xLCB0b3BOX2RmX3N2ZF8xJHJzdmQzMHRvcE5MaXN0X2RmLCAzMCkKY29tcGFyZV9pYmNmX3N2ZChyaWJjZnRvcE5fZGZfMSwgdG9wTl9kZl9zdmRfMSRyc3ZkNDB0b3BOTGlzdF9kZiwgNDApCmNvbXBhcmVfaWJjZl9zdmQocmliY2Z0b3BOX2RmXzEsIHRvcE5fZGZfc3ZkXzEkcnN2ZDUwdG9wTkxpc3RfZGYsIDUwKQoKcHJpbnQoIlp3ZWl0ZXIgRGF0ZW5zYXR6IikKY29tcGFyZV9pYmNmX3N2ZChyaWJjZnRvcE5fZGZfMiwgdG9wTl9kZl9zdmRfMiRyc3ZkMTB0b3BOTGlzdF9kZiwgMTApCmNvbXBhcmVfaWJjZl9zdmQocmliY2Z0b3BOX2RmXzIsIHRvcE5fZGZfc3ZkXzIkcnN2ZDIwdG9wTkxpc3RfZGYsIDIwKQpjb21wYXJlX2liY2Zfc3ZkKHJpYmNmdG9wTl9kZl8yLCB0b3BOX2RmX3N2ZF8yJHJzdmQzMHRvcE5MaXN0X2RmLCAzMCkKY29tcGFyZV9pYmNmX3N2ZChyaWJjZnRvcE5fZGZfMiwgdG9wTl9kZl9zdmRfMiRyc3ZkNDB0b3BOTGlzdF9kZiwgNDApCmNvbXBhcmVfaWJjZl9zdmQocmliY2Z0b3BOX2RmXzIsIHRvcE5fZGZfc3ZkXzIkcnN2ZDUwdG9wTkxpc3RfZGYsIDUwKQpgYGAKCkbDvHIgZGVuIGVyc3RlbiBEYXRlbnNhdHogd2VyZGVuIGJlaSBTVkQgV2VydCAxMCA3NiBnZW1laW5zYW1lIEVtcGZlaGx1bmdlbiBtaXQgSUJDRiBnZW5lcmllcnQuIERpZXMgZW50c3ByaWNodCBlaW5lbSBBbnRlaWwgdm9uIDE1LjYlIGRlciBFbXBmZWhsdW5nZW4gdm9tIFNWRCBNb2RlbGwuIEJpcyB6dXIgQW56YWhsIHZvbiA1MCBTaW5ndWzDpHJ3ZXJ0ZW4gc3RlaWd0IGRpZSBBbnphaGwgZ2VtZWluc2FtZXIgRW1wZmVobHVuZ2VuIGF1ZiAxMTcsIHdhcyBlaW5lbSBBbnRlaWwgdm9uIDI0JSBkZXIgRW1wZmVobHVuZ2VuIHZvbiBJQkNGIGVudHNwcmljaHQuIEbDvHIgZGVuIGVyc3RlbiBEYXRlbnNhdHogbMOkc3N0IHNpY2ggYWxzbyBlaW5lIHN0ZXRpZ2UgWnVuYWhtZSBkZXIgw5xiZXJlaW5zdGltbXVuZ2VuIHVuZCByZWxhdGl2ZXIgQW50ZWlsIGRlciDDnGJlcmVpbnN0aW1tdW5nZW4gZmVzdHN0ZWxsZW4uIEVpbmUgw6RobmxpY2hlIFp1bmFobWUgbMOkc3N0IHNpY2ggYXVjaCBiZWkgZGVtIHp3ZWl0ZW4gRGF0ZW5zYXR6IGZlc3RzdGVsbGVuLiBBbGxlcmRpbmdzIHNpbmQgZGllIEFuemFobCDDnGJlcmVpbnN0aW1tdW5nZW4gdGllZmVyIHVuZCB2b24gNDAgenUgNTAgU2luZ3Vsw6Ryd2VydGVuIHdlcmRlbiBrZWluZSB6dXPDpHR6bGljaGVuIMOcYmVyZWluc3RpbW11bmdlbiBtZWhyIGdlbmVyaWVydC4KCkJlaSBkZXIgU2luZ3Vsw6Ryd2VydGV6ZXJsZWd1bmcgdW5kIC1yZWtvbnN0cnVrdGlvbiB3ZXJkZW4gZGllIFJlc3VsdGF0ZSBtaXQgc3RlaWdlbmRlciBBbnphaGwgU2luZ3Vsw6Ryd2VydGUgenVlcnN0IHJhc2NoZW4sIGRhbm4gbGFuZ3NhbWVyIGJlc3Nlci4gRWluIMOkaG5saWNoZXMgVmVyaGFsdGVuIHNlaGVuIHdpciBhdWNoIGhpZXIuIE1pdCB6dW5laG1lbmRlbiBTaW5ndWzDpHJ3ZXJ0ZW4gd2lyZCBkaWUgw5xiZXJlaW5zdGltbXVuZyBtaXQgZGVuIEVtcGZlaGx1bmdlbiB2b24gSUJDRiBow7ZoZXIuIERhcyBsw6Rzc3QgbmljaHQgZGlyZWt0IGRlbiBTY2hsdXNzIHp1LCBkYXNzIElCQ0YgYmVzc2VyIGFscyBTVkQgbWl0IGVpbmVtIHRpZWZlbiBXZXJ0IGlzdCwgYWJlciBkaWUgUmVzdWx0YXRlIHdlcmRlbiBtaXQgenVuZWhtZW5kZW4gU2luZ3Vsw6Ryd2VydGVuIGJlc3Nlci4gCgojIDggSW1wbGVtZW50aWVydW5nIFRvcC1OIE1ldHJpa2VuCgpBdWZnYWJlIDggKERJWSkgCgojIyA4LjEgQ292ZXJhZ2VOIApgYGB7cn0KIyBUZXN0aW5nIGJlZm9yZSBjcmVhdGluZyB0aGUgQ292ZXJhZ2VOIGZ1bmN0aW9uCiMgY3JlYXRlIHJpYmNmIE1vZGVsCnJpYmNmXzggPC0gUmVjb21tZW5kZXIoTW92aWVMZW5zZSwgIklCQ0YiLCBwYXJhbT1saXN0KGs9IDMwLCBtZXRob2QgPSAiY29zaW5lIikpCiNyaWJjZl84CgojIHByZWRpY3QgYWxsIG1vdmllcyBmb3IgZXZlcnkgdXNlcgpyaWJjZnRvcE5MaXN0XzggPC0gcHJlZGljdChyaWJjZl84LCBNb3ZpZUxlbnNlLCBuPTE1KQojcmliY2Z0b3BOTGlzdF84CgojIGdldCB0aGUgbGlzdCBvZiB1bmlxdWUgaXRlbXMgZm9yIGFsbCB1c2VyCmxpc3RfaXRlbXNfOCA8LSB1bmlxdWUodW5saXN0KGFzKHJpYmNmdG9wTkxpc3RfOCwgImxpc3QiKSwgdXNlLm5hbWVzID0gRkFMU0UpKQojbGlzdF9pdGVtc184CgojIGdldCB0aGUgbGVuZ3RoIG9mIHRoaXMgbGlzdApsZW5faXRlbXNfOCA8LSBsZW5ndGgobGlzdF9pdGVtc184KQpwYXN0ZSgiVG9wLU4gTGlzdGUgZGVyIEt1bmRlbjoiLCBsZW5faXRlbXNfOCkKCiMgZ2V0IGxlbmd0aCBvZiB0b3RhbCBpdGVtcwpsZW5fYWxsX2l0ZW1zXzggPC0gZGltKE1vdmllTGVuc2UpWzJdCnBhc3RlKCJNZW5nZSBhbGxlciBQcm9kdWt0ZToiLCBsZW5fYWxsX2l0ZW1zXzgpCgojIGNhbGN1bGF0ZSBjb3ZlcmFnZU4KY292ZXJhZ2VOIDwtIGxlbl9pdGVtc184IC8gbGVuX2FsbF9pdGVtc184CnBhc3RlKCJjb3ZlcmFnZU4gZnVlciByaWJjZl84IG1vZGVsIG1pdCBuID0gMTUiLCByb3VuZChjb3ZlcmFnZU4sIDQpKQoKYGBgCgoKYGBge3J9CiMgY3JlYXRlIGEgZnVuY3Rpb24KY292ZXJhZ2VOIDwtIGZ1bmN0aW9uKG1vZGVsLCBuLCBkYXRhc2V0KSB7CiAgIyBwcmVkaWN0IGFsbCBtb3ZpZXMgZm9yIGV2ZXJ5IHVzZXIKICB0b3BOTGlzdCA8LSBwcmVkaWN0KG1vZGVsLCBkYXRhc2V0LCBuID0gbikKICAKICAjIGdldCB0aGUgbGlzdCBvZiB1bmlxdWUgaXRlbXMgZm9yIGFsbCB1c2VycwogIGxpc3RfaXRlbXMgPC0gdW5pcXVlKHVubGlzdChhcyh0b3BOTGlzdCwgImxpc3QiKSwgdXNlLm5hbWVzID0gRkFMU0UpKQogIAogICMgZ2V0IHRoZSBsZW5ndGggb2YgdGhpcyBsaXN0CiAgbGVuX2l0ZW1zIDwtIGxlbmd0aChsaXN0X2l0ZW1zKQogIAogICMgZ2V0IGxlbmd0aCBvZiB0b3RhbCBpdGVtcwogIGxlbl9hbGxfaXRlbXMgPC0gZGltKGRhdGFzZXQpWzJdCiAgCiAgIyBjYWxjdWxhdGUgY292ZXJhZ2UKICBjb3ZlcmFnZSA8LSBsZW5faXRlbXMgLyBsZW5fYWxsX2l0ZW1zCiAgCiAgcmV0dXJuKGNvdmVyYWdlKQp9CgpyaWJjZl84X2NvdmVyYWdlIDwtIGNvdmVyYWdlTihyaWJjZl84LCAxNSwgTW92aWVMZW5zZSkKcGFzdGUoIkNvdmVyYWdlIGZ1ZXIgcmliY2ZfOCBtb2RlbCBtaXQgbiA9IDE1OiIsIHJvdW5kKHJpYmNmXzhfY292ZXJhZ2UsIDQpKQoKYGBgCgpgYGB7cn0KIyBMaXN0IG9mIG4gdmFsdWVzCm5fdmFsIDwtIGMoNSwgMTAsIDE1LCAyMCwgMjUsIDMwKQpuX2RhdGFzZXQgPC0gYyhNb3ZpZUxlbnNlLCBNb3ZpZUxlbnNlQ29tcGFjdF8xLCBNb3ZpZUxlbnNlQ29tcGFjdF8yKQoKIyBDcmVhdGUgZW1wdHkgbGlzdApjb3ZlcmFnZV92YWx1ZXMgPC0gYygpCgpmb3IgKGRhdGFzZXQgaW4gbl9kYXRhc2V0KSB7CiAgIyBGb3IgbG9vcCB0byBpdGVyYXRlIG92ZXIgbl92YWx1ZXMKICByaWJjZl9pIDwtIFJlY29tbWVuZGVyKGRhdGFzZXQsICJJQkNGIiwgcGFyYW09bGlzdChrPSAzMCwgbWV0aG9kID0gImNvc2luZSIpKQogIHByaW50KGRhdGFzZXQpCiAgCiAgZm9yIChuIGluIG5fdmFsKSB7CiAgICAjIGNhbGN1bGF0ZSBjb3ZlcmFnZU4gd2l0aCByaWJjZl84CiAgICBjb3ZlcmFnZSA8LSBjb3ZlcmFnZU4ocmliY2ZfaSwgbiwgZGF0YXNldCkKICAgIGNvdmVyYWdlX3ZhbHVlcyA8LSBjKGNvdmVyYWdlX3ZhbHVlcywgY292ZXJhZ2UpCiAgICBwcmludChwYXN0ZSgiQ292ZXJhZ2UgZm9yIG4gPSIsIG4sICByb3VuZChjb3ZlcmFnZSwgNCkpKQogIH0KfQpgYGAKClNwYWNlIEJlaXRyYWcgw7xiZXIgQ292ZXJhZ2U6IGh0dHBzOi8vc3BhY2VzLnRlY2huaWsuZmhudy5jaC9zcGFjZXMvcmVjb21tZW5kZXItc3lzdGVtcy9iZWl0cmFlZ2UvcmVjb21tZW5kZXItc3lzdGVtLWV2YWx1aWVydW5nLWNvdmVyYWdlLXVuZC1ub3ZlbHR5LTEgCgpadXIgw5xiZXJwcsO8ZnVuZyBkZXMgRGF0ZW5zYXR6ZXMgaGFiZW4gd2lyIGRpZSBBbnphaGwgRmlsbWUgZGVzIGFrdHVlbGxlbiBNb3ZpZUxlbnNlIERhdGVuc2F0emVzIGF1c2dlZ2ViZW4sIGVyIGJldHLDpGd0IHdlaXRlcmhpbiAxJzY2NCBJdGVtcy4gVW5zZXJlIEJlcmVjaG51bmcgYW5oYW5kIGRlciBGb3JtZWwsIGRpZSBkdSBpbSBTcGFjZSB1bnRlciBCZWl0csOkZ2UgZ2Vwb3N0ZXQgaGFzdCwgZXJnaWJ0LCBkYXNzIDQxLjcgJSBkZXIgdm9yaGFuZGVuZW4gRmlsbWUgZW1wZm9obGVuIHdlcmRlbi4gRGllcyBiZXN0w6R0aWd0IHVuc2VyZSBFcmtlbm50bmlzIGF1cyBBdWZnYWJlIDUsIHdvbmFjaCBuaWNodCBhbGxlbiBVc2VybiBkaWUgZ2xlaWNoZW4gSXRlbXMgZW1wZm9obGVuIHdlcmRlbi4gQXVjaCBlcmtlbm5lbiB3aXIsIGRhcyBkdXJjaCBkaWUgc3RlaWdlbmRlIEFuemFobCBhbiBuIChBbnphaGwgSXRlbXMgRW1wZmVobHVuZykgZXJow7ZodCBzaWNoIGF1Y2ggZW50c3ByZWNoZW5kIENvdmVyYWdlLCB3YXMgYXVjaCBTaW5uIG1hY2h0IGRlbm4gZGVyIFrDpGhsZXIsIHNwcmljaCBkaWUgQW56YWhsIGVpbnppZ2FydGlnZSBGaWxtZW4gZ3LDtnNzZXIgc2luZCBiZWkgZWluZXIgZ3LDtnNzZXJlbiBBbnphaGwgbiwgZGEgbWVociBGaWxtZSBmw7xyIGplZGVuIFVzZXIgdm9yZ2VzY2hsYWdlbiB3ZXJkZW4uIERpZXNlIEJlb2JhY2h1bmcga8O2bm5lbiB3aXIgYmVpbSBrb21wbGV0dGVuIE1vdmllTGVuc2UgRGF0ZW5zYXR6IHNvd2llIGJlaSBkZW4gYmVpZGVuIHJlZHV6aWVydGVuIERhdGVuc8OkdHplbiBzZWhlbi4gIAoKIyMgOC4yIE5vdmVsdHlOCmBgYHtyfQpucmF0aW5ncyhNb3ZpZUxlbnNlKSAvIGxlbl9hbGxfaXRlbXNfOApgYGAKCmBgYHtyfQojIEtyZXV6dGFiZWxsZSBlcnN0ZWxsZW4gSXRlbSB2cyBBbnphaGwgUmF0aW5nCm1vdmllX3JhdGluZ3NfY291bnRzIDwtIHRhYmxlKE1vdmllTGVuc2VFREEkaXRlbSkKCiMgZGl2aWRpZXJlbiBkdXJjaCBHZXNhbXRhbnphaGwgSXRlbSBpbSBEYXRlbnNhdHogdW5kIGxvZ2FyaXRobWllcmVuCmxvZ19wb3B1bGFyaXR5IDwtIGxvZyhtb3ZpZV9yYXRpbmdzX2NvdW50cy9kaW0oTW92aWVMZW5zZUVEQSlbMl0pCgojIFZvbiBqZWRlbSBGaWxtIGRpZSBQb3B1bGFyaXR5IGFscyBEYXRhZnJhbWUKbG9nX3BvcF9kZiA8LSBkYXRhLmZyYW1lKGxvZ19wb3B1bGFyaXR5ID0gbG9nX3BvcHVsYXJpdHkpCmxvZ19wb3BfZGYKCiMgQW56YWhsIFVzZXIKU191c2VyIDwtIGRpbShNb3ZpZUxlbnNlKVsxXQpwYXN0ZSgiQW56YWhsIGFsbGVyIEt1bmRlbiIsIFNfdXNlcikKIyBBbnphaGwgSXRlbXMKTl9pdGVtIDwtIGRpbShNb3ZpZUxlbnNlKVsyXQpwYXN0ZSgiQW56YWhsIGFsbGVyIEl0ZW1zIiwgTl9pdGVtKQoKIyBNb2RlbApyaWJjZl9tb2RlbCA8LSBSZWNvbW1lbmRlcihNb3ZpZUxlbnNlLCAiSUJDRiIsIHBhcmFtPWxpc3Qoaz0gMzAsIG1ldGhvZCA9ICJjb3NpbmUiKSkKCiMgZ2V0IHByZWRpY3Rpb24gZm9yIGV2ZXJ5IHVzZXIKcmliY2ZfbW9kZWxfcHJlZGljdGlvbiA8LSBwcmVkaWN0KHJpYmNmXzgsIE1vdmllTGVuc2UsIG49MTUpCgpyaWJjZl9tb2RlbF9wcmVkaWN0aW9uX2xpc3QgPC0gdW5saXN0KGFzKHJpYmNmX21vZGVsX3ByZWRpY3Rpb24sICJsaXN0IiksIHVzZS5uYW1lcyA9IEZBTFNFKQoKIyBjcmVhdGUgYSBkYXRhZnJhbWUKcmliY2ZfbW9kZWxfcHJlZGljdGlvbl9kZiA8LSBkYXRhLmZyYW1lKHJpYmNmX21vZGVsX3ByZWRpY3Rpb25fbGlzdCkKCiMgam9pbiByaWJjZl9tb2RlbF9wcmVkaWN0aW9uX2RmIGFuZCBsb2dfcG9wX2RmCmpvaW5fZGYgPC0gbWVyZ2UocmliY2ZfbW9kZWxfcHJlZGljdGlvbl9kZiwgbG9nX3BvcF9kZiwgYnkueCA9ICJyaWJjZl9tb2RlbF9wcmVkaWN0aW9uX2xpc3QiLCBieS55ID0gImxvZ19wb3B1bGFyaXR5LlZhcjEiKQoKbm92ZWx0eSA8LSAtMS9OX2l0ZW0gKiBzdW0oam9pbl9kZiRsb2dfcG9wdWxhcml0eS5GcmVxKSAvIFNfdXNlcgoKbm92ZWx0eQoKYGBgCgoKYGBge3J9CiMgY3JlYXRlIGZ1bmN0aW9uCm5vdmVsdHkgPC0gZnVuY3Rpb24oZGF0YXNldCwgbW9kZWwsIG4pIHsKICAKICBtb3ZpZV9yYXRpbmdzX2NvdW50cyA8LSB0YWJsZShkYXRhc2V0JGl0ZW0pCiAgCiAgbG9nX3BvcHVsYXJpdHkgPC0gbG9nKG1vdmllX3JhdGluZ3NfY291bnRzL2RpbShkYXRhc2V0KVsyXSkKICAKICBsb2dfcG9wX2RmIDwtIGRhdGEuZnJhbWUobG9nX3BvcHVsYXJpdHkgPSBsb2dfcG9wdWxhcml0eSkKICAKICBTX3VzZXIgPC0gZGltKGRhdGFzZXQpWzFdCiAgTl9pdGVtIDwtIGRpbShkYXRhc2V0KVsyXQogIAogIHJpYmNmX21vZGVsX3ByZWRpY3Rpb24gPC0gcHJlZGljdChtb2RlbCwgZGF0YXNldCwgbikKICAKICByaWJjZl9tb2RlbF9wcmVkaWN0aW9uX2xpc3QgPC0gdW5saXN0KGFzKHJpYmNmX21vZGVsX3ByZWRpY3Rpb24sICJsaXN0IiksIHVzZS5uYW1lcyA9IEZBTFNFKQogIAogIHJpYmNmX21vZGVsX3ByZWRpY3Rpb25fZGYgPC0gZGF0YS5mcmFtZShyaWJjZl9tb2RlbF9wcmVkaWN0aW9uX2xpc3QpCiAgCiAgam9pbl9kZiA8LSBtZXJnZShyaWJjZl9tb2RlbF9wcmVkaWN0aW9uX2RmLCBsb2dfcG9wX2RmLCBieS54ID0gInJpYmNmX21vZGVsX3ByZWRpY3Rpb25fbGlzdCIsIGJ5LnkgPSAibG9nX3BvcHVsYXJpdHkuVmFyMSIpCiAgCiAgbm92ZWx0eSA8LSAtMS9OX2l0ZW0gKiBzdW0oam9pbl9kZiRsb2dfcG9wdWxhcml0eS5GcmVxKSAvIFNfdXNlcgogIAogIHJldHVybihub3ZlbHR5KQp9CgoKcmliY2ZfbW9kZWwgPC0gUmVjb21tZW5kZXIoTW92aWVMZW5zZSwgIklCQ0YiLCBwYXJhbT1saXN0KGs9IDMwLCBtZXRob2QgPSAiY29zaW5lIikpCgojbm92ZWx0eShNb3ZpZUxlbnNlRURBLCByaWJjZl9tb2RlbCwgMTUpCiMgZnVua3Rpb25pZXJ0IG5pY2h0CgpgYGAKCgoKV2llZGVyIGluIEFubGVobnVuZyBhbiBkZWluZSBCZXNjaHJlaWJ1bmcgZGl2aWRpZXJlbiB3aXIgZGllIEFuemFobCBzw6RtdGxpY2hlciBSYXRpbmdzIGR1cmNoIGRpZSBBbnphaGwgSXRlbXMuIERhcyBSZXN1bHRhdCBzYWd0IGF1cywgZGFzcyBydW5kIDYwIG1hbCBtZWhyIFJhdGluZ3MgYWJnZWdlYmVuIHd1cmRlbiwgYWxzIEl0ZW1zIHZvcmhhbmRlbiBzaW5kLgoKdG8tZG86IFN0aW1tdCBCZXJlY2hudW5nIHZvbiA4LjI/IFVudGVyc2NoaWVkbGljaGUgTGlzdGVubMOkbmdlbiEKCiMgOSBXYWhsIGRlcyBvcHRpbWFsZW4gUmVjb21tZW5kZXJzCkF1ZmdhYmUgOQojIyA5LjEgVmVyd2VuZGUgZsO8ciBkaWUgRXZhbHVpZXJ1bmcgMTAtZmFjaGUgS3JldXp2YWxpZGllcnVuZwpgYGB7cn0Kc2V0LnNlZWQoMTIzNCkKc2NoZW1lXzEgPC0gZXZhbHVhdGlvblNjaGVtZShNb3ZpZUxlbnNlQ29tcGFjdF8xLCBtZXRob2Q9ImNyb3NzLXZhbGlkYXRpb24iLCBrID0gMTAsIGdpdmVuPTMsIGdvb2RSYXRpbmc9NSkKc2NoZW1lXzIgPC0gZXZhbHVhdGlvblNjaGVtZShNb3ZpZUxlbnNlQ29tcGFjdF8yLCBtZXRob2Q9ImNyb3NzLXZhbGlkYXRpb24iLCBrID0gMTAsIGdpdmVuPTMsIGdvb2RSYXRpbmc9NSkKCnByaW50KCJFcnN0ZSBEYXRlbnJlZHVrdGlvbiIpCnNjaGVtZV8xCnByaW50KCJad2VpdGUgRGF0ZW5yZWR1a3Rpb24iKQpzY2hlbWVfMgoKCmBgYAoKIyMgOS4yIEJlZ3LDvG5kZSBkZWluZSBXYWhsIHZvbiBNZXRyaWtlbiB1bmQgTW9kZWxsCmBgYHtyfQphbGdvcml0aG1zIDwtIGxpc3QoImh5YnJpZCIgPSBsaXN0KG5hbWUgPSAiSFlCUklEIiwgcGFyYW0gPWxpc3QocmVjb21tZW5kZXJzID0gbGlzdChTVkQgPSBsaXN0KG5hbWU9IlNWRCIsIHBhcmFtPWxpc3QoayA9IDQwKSksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIFBPUFVMQVIgPSBsaXN0KG5hbWUgPSAiUE9QVUxBUiIsIHBhcmFtID0gTlVMTCkKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgKSkpLAogICAgICAgICAgICAgICAgICAgImxpYm1mIiA9IGxpc3QobmFtZT0iTElCTUYiLCBwYXJhbT1saXN0KGRpbT0xMCkpLAogICAgICAgICAgICAgICAgICAgInBvcHVsYXIgaXRlbXMiID0gbGlzdChuYW1lPSJQT1BVTEFSIiwgcGFyYW09TlVMTCksCiAgICAgICAgICAgICAgICAgICAidXNlci1iYXNlZCBDRiIgPSBsaXN0KG5hbWU9IlVCQ0YiLCBwYXJhbT1saXN0KG5uPTUwKSksCiAgICAgICAgICAgICAgICAgICAiaXRlbS1iYXNlZCBDRiIgPSBsaXN0KG5hbWU9IklCQ0YiLCBwYXJhbT1saXN0KGs9NTApKSwKICAgICAgICAgICAgICAgICAgICJTVkQ0MCIgPSBsaXN0KG5hbWU9IlNWRCIsIHBhcmFtPWxpc3QoayA9IDQwKSkpCgpwcmludCgiRXJzdGVyIERhdGVuc2F0eiIpCnJlc3VsdHNfMSA8LSBldmFsdWF0ZShzY2hlbWVfMSwgYWxnb3JpdGhtcywgdHlwZSA9ICJ0b3BOTGlzdCIsIG49YygxMCwgMTUsIDIwLCAyNSwgMzApKQpwcmludCgiWndlaXRlciBEYXRlbnNhdHoiKQpyZXN1bHRzXzIgPC0gZXZhbHVhdGUoc2NoZW1lXzIsIGFsZ29yaXRobXMsIHR5cGUgPSAidG9wTkxpc3QiLCBuPWMoMTAsIDE1LCAyMCwgMjUsIDMwKSkKYGBgCgpEaWVzZXIgUHJpbnQgc2FndCBudXIgYXVzLCB3aWUgbGFuZ2UgZWluemVsbmUgQmVyZWNobnVuZ2VuIGdlZGF1ZXJ0IGhhYmVuLiBXaXIga29tbWVudGllcmVuIGlobiBkZXNoYWxiIG5pY2h0IHZlcnRpZWZ0LgoKYGBge3J9CnBsb3QocmVzdWx0c18xLCBhbm5vdGF0ZT1jKDEsMyksIGxlZ2VuZD0idG9wbGVmdCIpCnBsb3QocmVzdWx0c18yLCBhbm5vdGF0ZT1jKDEsMyksIGxlZ2VuZD0idG9wbGVmdCIpCmBgYAoKIyMgOS4zIEFuYWx5c2llcmUgZGFzIGJlc3RlIE1vZGVsbCBmw7xyIFRvcC1OIFJlY29tbWVuZGF0aW9ucyBtaXQgTiA9IDEwLCAxNSwgMjAsIDI1IHVuZCAzMCwKSW4gQXVmZ2FiZSA5LjIgaGFiZW4gd2lyIGVpbmUgUk9DIEt1cnZlIGdlbcOkc3MgVm9ybGFnZSB2b24gUmVjb21tZW5kZXJsYWIgZXJzdGVsbHQuIEF1ZiBkZXIgeC1BY2hzZSBiZWZpbmRldCBzaWNoIGRpZSBGUFIgKGZhbHNlIHBvc2l0aXZlIHJhdGUpIHVuZCBhdWYgZGVyIHktQWNoc2UgZGllIFRQUiAodHJ1ZSBwb3NpdGl2ZSByYXRlKS4gRGllIE1ldGhvZGUgcG9wdWxhciBpdGVtcyBnZW5lcmllcnQgZGVuIHRpZWZzdGVuIEZQUiB1bmQgVFBSIGbDvHIgTiA9IDEwLiBEYSBkaWUgc2VsYmUgTWV0aG9kZSBiaXMgTiA9IDMwIGRhcyBiZXN0ZSBWZXJow6RsdG5pcyAoaMO2Y2hzdGUgS3VydmUgdW5kIGjDtmNoc3RlciBBVUMpIGF1ZndlaXN0LCBpc3QgZGllcyBkYXMgYmVzdGUgTW9kZWxsLiBEZW4gendlaXRlbiBQbGF0eiB0ZWlsZW4gc2ljaCBkYXMgaHlicmlkZSBNb2RlbGwgdW5kIFNWRCBtaXQgNDAgV2VydGVuLiBEaWUgS3VydmUgZGVyIGJlaWRlbiBsaWVndCBzdMOkbmRpZyDDvGJlcmVpbmFuZGVyLiBBbSBzY2hsZWNodGVzdGVuIGhhdCBkYXMgaXRlbS1iYXNlZCBDRiBNb2RlbCBhYmdlc2Nobml0dGVuLCB3ZWxjaGVzIHp3YXIgZ2xlaWNoIHdpZSBkYXMgdXNlci1iYXNlZCBhbmdlZmFuZ2VuIGhhdCwgZGFubiBhYmVyIGRpZSBUUFIgbmljaHQgbWVociBnbGVpY2ggc3RhcmsgdmVyYmVzc2VybiBrb25udGUuCgpCZWltIHp3ZWl0ZW4gRGF0ZW5zYXR6IGhhYmVuIGRpZSBNb2RlbGwgZGFzIGdsZWljaGUgUmVzdWx0YXQgZ2VuZXJpZXJ0LCBhdXNzZXIsIGRhc3MgZGllIG1pdHRsZXJlbiBNb2RlbGwgbsOkaGVyIHp1c2FtbWVuIGxpZWdlbiB1bmQgaXRlbS1iYXNlZCBzdMOkcmtlciB6dXLDvGNrIGxpZWd0LiBGw7xyIGRpZXNlbiBEYXRlbnNhdHoga29ubnRlIGRhcyBNb2RlbGwgdm9uIFJlY29tbWVuZGVybGFiIGtlaW4gUmVzdWx0YXQgZsO8ciB1c2VyLWJhc2VkIGJlcmVjaG5lbi4gRGllIGVudHNwcmVjaGVuZGUgRmVobGVybWVsZHVuZyBpc3QgYXVjaCBiZWkgZGVyIEdlbmVyaWVydW5nIGluIEF1ZmdhYmUgOS4yIGF1ZmdldGF1Y2h0LgoKTmFjaGRlbSwgd2FzIHdpciDDvGJlciBSZWNvbW1lbmRlciBTeXN0ZW1zIGdlbGVybnQgaGFiZW4sIGhhYmVuIHdpciBlaWdlbnRsaWNoIG5pY2h0IGVyd2FydGV0LCBkYXNzIGRhcyBwb3B1bGFyIE1vZGVsbCBhbSBiZXN0ZW4gYWJzY2huZWlkZXQuIFdpciBoYWJlbiBlaGVyIGVyd2FydGV0LCBkYXNzIGR1IHVucyBlaW5lbiBEYXRlbnNhdHogZ2lic3QsIGJlaSBkZW0gdXNlci0gdW5kIGl0ZW0tYmFzZWQgYmVzc2VyIGFic2NobmVpZGVuLCB3ZWlsIHdpciBpbSBMZXJubWF0ZXJpYWwgdmllbCBkYXLDvGJlciBnZWxlcm50IGhhYmVuLiBEYSBkYXMgUmVzdWx0YXQgZ2VuYXUgdW1nZWtlaHJ0IGlzdCwgaGFiZW4gd2lyIMO8YmVycHLDvGZ0LCBvYiBlaW4gRmVobGVyIHZvcmxpZWdlbiBrw7ZubnRlLCB3aXIgaGFiZW4gZGllc2VuIGFiZXIgbmljaHQgZ2VmdW5kZW4uCgpCZWltIFZlcmdsZWljaCBkZXIgYmVpZGVuIERhdGVuc8OkdHplIGdlaGVuIHdpciBkYXZvbiBhdXMsIGRhc3MgZGllIHRpZWZlciBUUFIgdW5kIGxlaWNodCBow7ZoZXJlIEZQUiBkYXZvbiBrb21tdCwgZGFzcyBkaWUgTWF0cml4IGVya2VubmJhciBzcGFyc2VyIGlzdCB1bmQgZGFtaXQgd2VuaWdlciBUcmFpbmluZ3NkYXRlbiB2b3JoYW5kZW4gc2luZC4KCiMjIDkuNCBPcHRpbWllcmUgZGVpbiBiZXN0ZXMgTW9kZWxsIGhpbnNpY2h0bGljaCBIeXBlcnBhcmFtZXRlcm4KYGBge3J9CmFsZ29yaXRobXNpbXByb3ZlZHJlY29tIDwtIGxpc3QoInBvcHVsYXIgaXRlbXMgY2VudGVyIiA9IGxpc3QobmFtZT0iUE9QVUxBUiIsIHBhcmFtPU5VTEwpLAogICAgICAgICAgICAgICAgICAgInBvcHVsYXIgaXRlbXMgWi1zY29yZSIgPSBsaXN0KG5hbWU9IlBPUFVMQVIiLCBwYXJhbT1saXN0KG5vcm1hbGl6ZT0iWi1zY29yZSIpKSkKCnJlc3VsdHNpbXByb3ZlZHJlY29tXzEgPC0gZXZhbHVhdGUoc2NoZW1lXzEsIGFsZ29yaXRobXNpbXByb3ZlZHJlY29tLCB0eXBlID0gInRvcE5MaXN0Iiwgbj1jKDEwLCAxNSwgMjAsIDI1LCAzMCkpCnJlc3VsdHNpbXByb3ZlZHJlY29tXzIgPC0gZXZhbHVhdGUoc2NoZW1lXzIsIGFsZ29yaXRobXNpbXByb3ZlZHJlY29tLCB0eXBlID0gInRvcE5MaXN0Iiwgbj1jKDEwLCAxNSwgMjAsIDI1LCAzMCkpCmBgYAoKYGBge3J9CnBsb3QocmVzdWx0c2ltcHJvdmVkcmVjb21fMSwgYW5ub3RhdGU9YygxLDMpLCBsZWdlbmQ9InRvcGxlZnQiKQpwbG90KHJlc3VsdHNpbXByb3ZlZHJlY29tXzIsIGFubm90YXRlPWMoMSwzKSwgbGVnZW5kPSJ0b3BsZWZ0IikKYGBgCgpOYWNoZGVtIHdpciBmZXN0Z2VzdGVsbHQgaGFiZW4sIGRhc3MgcG9wdWxhciBkYXMgYmVzdGUgTW9kZWxsIGlzdCwgYmxpZWIgdW5zIGbDvHIgZGllIE9wdGltaWVydW5nIGRlciBIeXBlcnBhcmFtZXRlciBudXIgZGllIEFucGFzc3VuZyBkZXIgTm9ybWllcnVuZy4gRWluZXJzZWl0cyBzdGVodCBkaWUga2xhc3Npc2NoZSBaZW50cmFsaXNpZXJ1bmcgZGVyIERhdGVuIHVuZCBhbmRlcmVyc2VpdHMgZGllIE5vcm1pZXJ1bmcgbWl0dGVscyBaLVNjb3JlIHp1ciBWZXJmw7xndW5nLiBEYXMgTW9kZWxsIGhhdCBtaXQgZGVuIHNlbGJlbiBOIFdlcnRlbiBrZWluZSBzaWNodGJhcmVuIFVudGVyc2NoaWVkZSB6d2lzY2hlbiBkZW4gTm9ybWllcnVuZ3NtZXRob2RlbiBnZW5lcmllcnQuIERhcyBnaWx0IGbDvHIgYmVpZGUgRGF0ZW5zw6R0emUuIERhIGRpZSBkYXJnZXN0ZWxsdGUgQmVyZWNobnVuZ3NkYXVlciBmw7xyIGJlaWRlIE1ldGhvZGVuIGdsZWljaCBsYW5nIGlzdCwgZW50c2NoZWlkZW4gd2lyIHVucyBmw7xyIGRpZSBub3JtYWxlIE5vcm1pZXJ1bmcsIGRhIGRpZXNlIGVpbmZhY2hlciB6dSBiZXJlY2huZW4gaXN0LiBXaXIgZXJ3YXJ0ZW4gZGFkdXJjaCBlaW5lbiBQZXJmb3JtYW5jZXZvcnRlaWwgYmVpIGdyw7Zzc2VyZW4gRGF0ZW5zw6R0emVuLgoKIyAxMCBJbXBsZW1lbnRpZXJ1bmcgVG9wLU4gTW9uaXRvcgpBdWZnYWJlIDEwIChESVkpOiBVbnRlcnN1Y2hlIGRpZSByZWxhdGl2ZSDDnGJlcmVpbnN0aW1tdW5nIHp3aXNjaGVuClRvcC1OIEVtcGZlaGx1bmdlbiB1bmQgcHLDpGZlcmllcnRlbiBGaWxtZW4gZsO8ciA0IHVudGVyc2NoaWVkbGljaGUKTW9kZWxsZSAoei5CLiBJQkNGIHVuZCBVQkNGIG1pdCB1bnRlcnNjaGllZGxpY2hlbiDDhGhubGljaGtlaXRzbWV0cmlrZW4gLyBOYWNoYmFyc2NoYWZ0ZW4gc293aWUgU1ZEIG1pdCB1bnRlcnNjaGllZGxpY2hlcgpEaW1lbnNpb25hbGl0w6R0c3JlZHVrdGlvbikuCgojIyAxMC4xIEZpeGllcmUgMjAgenVmw6RsbGlnIGdld8OkaGx0ZSBUZXN0a3VuZGVuIGbDvHIgYWxsZSBNb2RlbGx2ZXJnbGVpY2hlLApgYGB7cn0KIyBUZXN0aW5nIGJlZm9yZSBjcmVhdGluZyBmdW5jdGlvbgojIHNlbGVjdCAyMCByYW5kb20gdXNlcnMKc2V0LnNlZWQoMTIzNCkKdGVzdFVzZXJzIDwtIHNhbXBsZSgxOm5yb3coTW92aWVMZW5zZSksIDIwKQp0ZXN0VXNlcnMKCiMgZmlsdGVyIE1vdmllTGVuc2UgYnkgdGVzdFVzZXJzCk1vdmllTGVuc2VUZXN0IDwtIE1vdmllTGVuc2VbdGVzdFVzZXJzLF0gCk1vdmllTGVuc2VUZXN0CmBgYAoKSW4gZGllc2VyIFplbGxlIGhhYmVuIHdpciBkaWUgQXVzd2FobCBkZXIgenVmw6RsbGlnZW4gS3VuZGVuIGdldGVzdGV0LCBiZXZvciB3aXIgZWluZSBGdW5rdGlvbiBlcnN0ZWxsZW4uCgpgYGB7cn0KIyBjcmVhdGUgZnVuY3Rpb24gZm9yIHNlbGVjdCByYW5kb20gdXNlcnMKc2VsZWN0X3JhbmRvbV91c2VycyA8LSBmdW5jdGlvbihkYXRhLCBudW1fdXNlcnMsIHNlZWQpIHsKICBzZXQuc2VlZChzZWVkKQogIHRlc3RVc2VycyA8LSBzYW1wbGUoMTpucm93KGRhdGEpLCBudW1fdXNlcnMpCiAgZGF0YVRlc3QgPC0gZGF0YVt0ZXN0VXNlcnMsXQogIAogIHJldHVybihsaXN0KHRlc3RVc2VycywgZGF0YVRlc3QpKQp9CgpzZWxlY3RfcmFuZG9tX3VzZXIgPC0gc2VsZWN0X3JhbmRvbV91c2VycyhNb3ZpZUxlbnNlLCAyMCwgMTIzNCkKCnRlc3RVc2VycyA8LSBzZWxlY3RfcmFuZG9tX3VzZXJbWzFdXQpNb3ZpZUxlbnNlVGVzdCA8LSBzZWxlY3RfcmFuZG9tX3VzZXJbWzJdXQp0ZXN0VXNlcnMKTW92aWVMZW5zZVRlc3QKYGBgCgpOdW4gaGFiZW4gd2lyIGRpZSBGdW5rdGlvbiBlcnN0ZWxsdCB1bmQgc2llIGF1ZiBkZW0gZ2xlaWNoZW4gRGF0ZW5zYXR6IGdldGVzdGV0LiBEYXMgUmVzdWx0YXQgaXN0IHdpZWRlciBkYXMgZ2xlaWNoZS4KCmBgYHtyfQojIE1ha2UgZm9yIGJvdGggZGF0YXNldHMKIyBkYXRhc2V0IDEKc2VsZWN0X3JhbmRvbV91c2VyXzEgPC0gc2VsZWN0X3JhbmRvbV91c2VycyhNb3ZpZUxlbnNlQ29tcGFjdF8xLCAyMCwgMTIzNCkKCnRlc3RVc2Vyc18xIDwtIHNlbGVjdF9yYW5kb21fdXNlcl8xW1sxXV0KTW92aWVMZW5zZVRlc3RfMSA8LSBzZWxlY3RfcmFuZG9tX3VzZXJfMVtbMl1dCnRlc3RVc2Vyc18xCk1vdmllTGVuc2VUZXN0XzEKCiMgZGF0YXNldCAyCnNlbGVjdF9yYW5kb21fdXNlcl8yIDwtIHNlbGVjdF9yYW5kb21fdXNlcnMoTW92aWVMZW5zZUNvbXBhY3RfMiwgMjAsIDEyMykKCnRlc3RVc2Vyc18yIDwtIHNlbGVjdF9yYW5kb21fdXNlcl8yW1sxXV0KTW92aWVMZW5zZVRlc3RfMiA8LSBzZWxlY3RfcmFuZG9tX3VzZXJfMltbMl1dCnRlc3RVc2Vyc18yCk1vdmllTGVuc2VUZXN0XzIKCmBgYAoKV2lyIGhhYmVuIG51biBkaWUgRnVudGtpb24gYXVmIGJlaWRlIHJlZHV6aWVydGVuIERhdGVuc8OkdHplIGFuZ2V3ZW5kZXQuIERhIGRlciBzZWxiZSBTZWVkIHRyb3R6ZGVtIGRpZSBnbGVpY2hlbiBVc2VyIGF1c2dld8OkaGx0IGhhdCwgbXVzc3RlbiB3aXIgaWhuIHVudGVyc2NoaWVkbGljaCBkZWZpbmllcmVuLiAKCiMjIDEwLjIgQmVzdGltbWUgZGVuIEFudGVpbCBkZXIgVG9wLU4gRW1wZmVobHVuZyBuYWNoIEdlbnJlcyBwcm8gS3VuZGUsCmBgYHtyfQojIE1vZGVsbGUgZXJzdGVsbGVuCnJpYmNmXzEwIDwtIFJlY29tbWVuZGVyKE1vdmllTGVuc2VUZXN0LCAiSUJDRiIsIHBhcmFtPWxpc3Qoaz0gMzAsIG1ldGhvZCA9ICJjb3NpbmUiKSkKCnJ1YmNmXzEwIDwtIFJlY29tbWVuZGVyKE1vdmllTGVuc2VUZXN0LCAiVUJDRiIsIHBhcmFtZXRlcj1saXN0KG1ldGhvZCA9ICJFdWNsaWRlYW4iLCBubiA9IDEwLCBub3JtYWxpemUgPSAiWi1zY29yZSIpKQoKcnN2ZF8zIDwtIFJlY29tbWVuZGVyKE1vdmllTGVuc2VUZXN0LCAiU1ZEIiwgcGFyYW0gPSBsaXN0KGsgPSAzKSkKCnJzdmRfNSA8LSBSZWNvbW1lbmRlcihNb3ZpZUxlbnNlVGVzdCwgIlNWRCIsIHBhcmFtID0gbGlzdChrID0gNSkpCmBgYAoKSGllciBoYWJlbiB3aXIgZGllIHZpZXIgTW9kZWxsZSB0cmFpbmllcnQuCgpgYGB7cn0KI3JpYmNmXzEwCiMgcHJlZGljdCBUb3AtTiBpdGVtcyBmb3IgZXZlcnkgdXNlcgpyaWJjZnRvcE5MaXN0XzEwIDwtIHByZWRpY3QocmliY2ZfMTAsIE1vdmllTGVuc2VUZXN0LCBuPTE1KQoKIyBjcmVhdGUgYSBsaXN0IHdpdGggdGhlIHRvcE4gaXRlbXMgZm9yIGV2ZXJ5IHVzZXIKcmliY2Z0b3BOTGlzdF8xMF9saXN0IDwtIGFzKHJpYmNmdG9wTkxpc3RfMTAsICJsaXN0IikKCiMgY3JlYXRlIGEgdGliYmxlIHdpdGggdGhlIHRvcE4gaXRlbXMgZm9yIGV2ZXJ5IHVzZXIKcmliY2Z0b3BOTGlzdF8xMF90aWJibGUgPC0gYXNfdGliYmxlKHJpYmNmdG9wTkxpc3RfMTBfbGlzdCkKCiMgdHJhbnNmb3JtIHRoZSB0aWJibGUgdG8gYSBkYXRhIGZyYW1lCnJpYmNmdG9wTkxpc3RfMTBfZGYgPC0gYXMuZGF0YS5mcmFtZShyaWJjZnRvcE5MaXN0XzEwX3RpYmJsZSkKCiMgcmVwbGFjZSBjb2xuYW1lIHdpdGggdGVzdFVzZXJzCmNvbG5hbWVzKHJpYmNmdG9wTkxpc3RfMTBfZGYpIDwtIHRlc3RVc2VycwoKIyB0cmFuc3Bvc2UgZGF0YSBmcmFtZQpyaWJjZnRvcE5MaXN0XzEwX2RmX3RyYW5zcG9zZWQgPC0gdChyaWJjZnRvcE5MaXN0XzEwX2RmKQoKIyBjaGFuZ2UgcmliY2Z0b3BOTGlzdF8xMF9kZl90cmFuc3Bvc2VkIHRvIGEgdGliYmxlCnJpYmNmdG9wTkxpc3RfMTBfZGZfdHJhbnNwb3NlZF90aWJibGUgPC0gYXNfdGliYmxlKHJpYmNmdG9wTkxpc3RfMTBfZGZfdHJhbnNwb3NlZCkKCiMgYWRkIGEgY29sdW1uIHdpdGggdGhlIHRlc3RVc2VycwpyaWJjZnRvcE5MaXN0XzEwX2RmX3RyYW5zcG9zZWRfdGliYmxlJHRlc3RVc2VycyA8LSB0ZXN0VXNlcnMKCiMgcGl2b3QgbG9uZ2VyIGRhdGFmcmFtZQpyaWJjZnRvcE5MaXN0XzEwX2RmX3RyYW5zcG9zZWRfdGliYmxlX3Bpdm90IDwtIHBpdm90X2xvbmdlcihyaWJjZnRvcE5MaXN0XzEwX2RmX3RyYW5zcG9zZWRfdGliYmxlLCBjb2xzID0gMToxNSwgbmFtZXNfdG8gPSAidG9wTiIsIHZhbHVlc190byA9ICJpdGVtSUQiKQoKIyBnZXQgZ2VucmUgZnJvbSBlYWNoIGl0ZW0KcmliY2Z0b3BOTGlzdF8xMF9kZl90cmFuc3Bvc2VkX3RpYmJsZV9waXZvdF9nZW5yZSA8LSBsZWZ0X2pvaW4ocmliY2Z0b3BOTGlzdF8xMF9kZl90cmFuc3Bvc2VkX3RpYmJsZV9waXZvdCwgTW92aWVMZW5zZU1ldGEsIGJ5ID0gYygiaXRlbUlEIiA9ICJ0aXRsZSIpKQpyaWJjZnRvcE5MaXN0XzEwX2RmX3RyYW5zcG9zZWRfdGliYmxlX3Bpdm90X2dlbnJlCgojIGRyb3AgY29sdW1ucyB0b3BOLCB5ZWFyLCB1cmwKcmliY2Z0b3BOTGlzdF8xMF9kZl90cmFuc3Bvc2VkX3RpYmJsZV9waXZvdF9nZW5yZSA8LSBzZWxlY3QocmliY2Z0b3BOTGlzdF8xMF9kZl90cmFuc3Bvc2VkX3RpYmJsZV9waXZvdF9nZW5yZSwgLXRvcE4sIC15ZWFyLCAtdXJsLCAtaXRlbUlEKQpyaWJjZnRvcE5MaXN0XzEwX2RmX3RyYW5zcG9zZWRfdGliYmxlX3Bpdm90X2dlbnJlCmBgYAoKRGllc2UgdW5kIGRpZSBuw6RjaHN0ZSBaZWxsZSBzdGVsbHQgdW5zZXIgVGVzdGluZyBkZXIgQXVzd2VydHVuZyBkYXIuCgpgYGB7cn0KIyBwaXZvdCBsb25nZXIgZGF0YWZyYW1lCnRvcG5tb25pdG9yX3JlY29tIDwtIHJpYmNmdG9wTkxpc3RfMTBfZGZfdHJhbnNwb3NlZF90aWJibGVfcGl2b3RfZ2VucmUgJT4lIGdyb3VwX2J5KHRlc3RVc2VycykgJT4lCiAgc3VtbWFyaXNlKGFjcm9zcyhldmVyeXRoaW5nKCksIH4gc3VtKC4sIGlzLm5hKC4pLCAwKSkpCnRvcG5tb25pdG9yX3JlY29tCmBgYAoKYGBge3J9CmdldF90b3Bfbl9pdGVtcyA8LSBmdW5jdGlvbihtb2RlbCwgZGF0YXNldCwgdXNlcmxpc3QpIHsKICAjIHByZWRpY3QgVG9wLU4gaXRlbXMgZm9yIGV2ZXJ5IHVzZXIKICB0b3Bfbl9saXN0IDwtIHByZWRpY3QobW9kZWwsIGRhdGFzZXQsIG4gPSAxNSkKICBwcmludCh0b3Bfbl9saXN0KQogIAogICMgY3JlYXRlIGEgbGlzdCB3aXRoIHRoZSB0b3BOIGl0ZW1zIGZvciBldmVyeSB1c2VyCiAgdG9wX25fbGlzdF9saXN0IDwtIGFzKHRvcF9uX2xpc3QsICJsaXN0IikKICAKICAjIGNyZWF0ZSBhIHRpYmJsZSB3aXRoIHRoZSB0b3BOIGl0ZW1zIGZvciBldmVyeSB1c2VyCiAgdG9wX25fbGlzdF90aWJibGUgPC0gYXNfdGliYmxlKHRvcF9uX2xpc3RfbGlzdCkKICAKICAjIHRyYW5zZm9ybSB0aGUgdGliYmxlIHRvIGEgZGF0YSBmcmFtZQogIHRvcF9uX2xpc3RfZGYgPC0gYXMuZGF0YS5mcmFtZSh0b3Bfbl9saXN0X3RpYmJsZSkKICAKICAjIHJlcGxhY2UgY29sbmFtZSB3aXRoIHRlc3RVc2VycwogIGNvbG5hbWVzKHRvcF9uX2xpc3RfZGYpIDwtIHVzZXJsaXN0CiAgCiAgIyB0cmFuc3Bvc2UgZGF0YSBmcmFtZQogIHRvcF9uX2xpc3RfZGZfdHJhbnNwb3NlZCA8LSB0KHRvcF9uX2xpc3RfZGYpCiAgCiAgIyBjaGFuZ2UgdG9wX25fbGlzdF9kZl90cmFuc3Bvc2VkIHRvIGEgdGliYmxlCiAgdG9wX25fbGlzdF9kZl90cmFuc3Bvc2VkX3RpYmJsZSA8LSBhc190aWJibGUodG9wX25fbGlzdF9kZl90cmFuc3Bvc2VkKQogIAogICMgYWRkIGEgY29sdW1uIHdpdGggdGhlIHRlc3RVc2VycwogIHRvcF9uX2xpc3RfZGZfdHJhbnNwb3NlZF90aWJibGUkdGVzdFVzZXJzIDwtIHVzZXJsaXN0CiAgCiAgIyBwaXZvdCBsb25nZXIgZGF0YWZyYW1lCiAgdG9wX25fbGlzdF9kZl90cmFuc3Bvc2VkX3RpYmJsZV9waXZvdCA8LSBwaXZvdF9sb25nZXIodG9wX25fbGlzdF9kZl90cmFuc3Bvc2VkX3RpYmJsZSwgY29scyA9IDE6MTUsIG5hbWVzX3RvID0gInRvcE4iLCB2YWx1ZXNfdG8gPSAiaXRlbUlEIikKICAKICAjIGdldCBnZW5yZSBmcm9tIGVhY2ggaXRlbQogIHRvcF9uX2xpc3RfZGZfdHJhbnNwb3NlZF90aWJibGVfcGl2b3RfZ2VucmUgPC0gbGVmdF9qb2luKHRvcF9uX2xpc3RfZGZfdHJhbnNwb3NlZF90aWJibGVfcGl2b3QsIE1vdmllTGVuc2VNZXRhLCBieSA9IGMoIml0ZW1JRCIgPSAidGl0bGUiKSkKICAKICAjIGRyb3AgY29sdW1ucyB0b3BOLCB5ZWFyLCB1cmwKICB0b3Bfbl9saXN0X2RmX3RyYW5zcG9zZWRfdGliYmxlX3Bpdm90X2dlbnJlIDwtIHNlbGVjdCh0b3Bfbl9saXN0X2RmX3RyYW5zcG9zZWRfdGliYmxlX3Bpdm90X2dlbnJlLCAtdG9wTiwgLXllYXIsIC11cmwsIC1pdGVtSUQpCiAgCiAgcmV0dXJuKHRvcF9uX2xpc3RfZGZfdHJhbnNwb3NlZF90aWJibGVfcGl2b3RfZ2VucmUpCn0KCnJpYmNmdG9wTkxpc3RfMTBfZGZfdHJhbnNwb3NlZF90aWJibGVfcGl2b3RfZ2VucmUgPC0gZ2V0X3RvcF9uX2l0ZW1zKHJpYmNmXzEwLCBNb3ZpZUxlbnNlVGVzdCwgdGVzdFVzZXJzKQpyaWJjZnRvcE5MaXN0XzEwX2RmX3RyYW5zcG9zZWRfdGliYmxlX3Bpdm90X2dlbnJlCmBgYAogRGllc2UgWmVsbGUgd2FyIGVpbiBUZXN0aW5nIGRlciBuZXUgZ2VzY2hyaWViZW5lbiBGdW5rdGlvbi4KCmBgYHtyfQojIGJvdGggZGF0YXNldHMKbl9kYXRhc2V0IDwtIGxpc3QoTW92aWVMZW5zZUNvbXBhY3RfMSwgTW92aWVMZW5zZUNvbXBhY3RfMikKCmNudCA8LSAxCnRvcF9uX2xpc3QgPC0gbGlzdCgpCgoKZm9yIChpIGluIDE6bGVuZ3RoKG5fZGF0YXNldCkpIHsKICBzZWxlY3RfcmFuZG9tX3VzZXIgPC0gc2VsZWN0X3JhbmRvbV91c2VycyhuX2RhdGFzZXRbW2ldXSwgMjAsIDEyMzQpCiAgdGVzdFVzZXJfaSA8LSBzZWxlY3RfcmFuZG9tX3VzZXJbWzFdXQogIE1vdmllTGVuc2VfaSA8LSBzZWxlY3RfcmFuZG9tX3VzZXJbWzJdXQogIAogIHJpYmNmXzEwIDwtIFJlY29tbWVuZGVyKG5fZGF0YXNldFtbaV1dLCAiSUJDRiIsIHBhcmFtPWxpc3Qoaz0gMzAsIG1ldGhvZCA9ICJjb3NpbmUiKSkKCiAgcnViY2ZfMTAgPC0gUmVjb21tZW5kZXIobl9kYXRhc2V0W1tpXV0sICJVQkNGIiwgcGFyYW1ldGVyPWxpc3QobWV0aG9kID0gIkV1Y2xpZGVhbiIsIG5uID0gMTAsIG5vcm1hbGl6ZSA9ICJaLXNjb3JlIikpCgogIHJzdmRfMyA8LSBSZWNvbW1lbmRlcihuX2RhdGFzZXRbW2ldXSwgIlNWRCIsIHBhcmFtID0gbGlzdChrID0gMykpCgogIHJzdmRfNSA8LSBSZWNvbW1lbmRlcihuX2RhdGFzZXRbW2ldXSwgIlNWRCIsIHBhcmFtID0gbGlzdChrID0gNSkpCiAgCiAgbl9tb2RlbHMgPC0gbGlzdChyaWJjZl8xMCwgcnViY2ZfMTAsIHJzdmRfMywgcnN2ZF81KQogIHByaW50KG5fZGF0YXNldFtpXSkKICBmb3IgKGogaW4gMTpsZW5ndGgobl9tb2RlbHMpKSB7CiAgICBnZXRfdG9wX25faXRlbXNfZGYgPC0gZ2V0X3RvcF9uX2l0ZW1zKG5fbW9kZWxzW1tqXV0sIE1vdmllTGVuc2VfaSwgdGVzdFVzZXJfaSkKICAgIHRvcG5tb25pdG9yX3JlY29tIDwtIGdldF90b3Bfbl9pdGVtc19kZiAlPiUgZ3JvdXBfYnkodGVzdFVzZXJzKSAlPiUKICAgIHN1bW1hcmlzZShhY3Jvc3MoZXZlcnl0aGluZygpLCB+IHN1bSguLCBpcy5uYSguKSwgMCkpKQogICAgCiAgICB0b3Bfbl9saXN0W1tjbnRdXSA8LSB0b3BubW9uaXRvcl9yZWNvbQogICAgY250IDwtIGNudCArIDEKICB9Cn0gIAogIAoKdG9wX25fbGlzdApgYGAKRGllc2UgQXVzd2VydHVuZyBzdGVsbHQgbnVuIGRpZSBWZXJ0ZWlsdW5nIGRlciB0b3AgZW1wZm9obGVuZW4gR2VucmUgZsO8cnMgZGllIGVpbnplbG5lbiBLdW5kZW4gZGFyLiBJbSBlcnN0ZW4gRGF0YWZyYW1lIHdpcmQgZXJzaWNodGxpY2gsIGRhc3MgZsO8ciBVc2VyIDQgdmllcm1hbCBBY3Rpb24gRmlsbWUgZW1wZm9obGVuIHd1cmRlbi4gRHJhbWEgRmlsbWUgYmVmYW5kZW4gc2ljaCBnYXIgMTAgbWFsIHVudGVyIGRlbiBGaWxtZW4uCgojIyAxMC4zIEJlc3RpbW1lIHBybyBLdW5kZSBkZW4gQW50ZWlsIG5hY2ggR2VucmVzIHNlaW5lciBUb3AtRmlsbWUKKD1GaWxtZSBtaXQgYmVzdGVuIEJld2VydHVuZ2VuKSwKCmBgYHtyfQp0b3BubW9uaXRvcl9mYXZfbW92aWVzIDwtIE1vdmllTGVuc2VFREFfSm9pbmVkICU+JSBmaWx0ZXIodXNlciAlaW4lIHRlc3RVc2VycywgcmF0aW5nID09IDUpICU+JSBncm91cF9ieSh1c2VyKSAlPiUgc3VtbWFyaXNlKGFjcm9zcyhjKHVua25vd24sIEFjdGlvbiwgQWR2ZW50dXJlLCBBbmltYXRpb24sIGBDaGlsZHJlbidzYCwgQ29tZWR5LCBDcmltZSwgRG9jdW1lbnRhcnksIERyYW1hLCBGYW50YXN5LCBgRmlsbS1Ob2lyYCwgSG9ycm9yLCBNdXNpY2FsLCBNeXN0ZXJ5LCBSb21hbmNlLCBgU2NpLUZpYCwgVGhyaWxsZXIsIFdhciwgV2VzdGVybiksc3VtKSkgJT4lIG11dGF0ZSh1c2VyID0gYXMubnVtZXJpYyh1c2VyKSkgJT4lICBhcnJhbmdlKHVzZXIpCnRvcG5tb25pdG9yX2Zhdl9tb3ZpZXMKYGBgCkhpZXJiZWkgaGFuZGVsdCBlcyBzaWNoIHdpZWRlciB1bSBlaW4gUmVzdWx0YXQgZWluZXMgVGVzdHMuCgpgYGB7cn0KZ2V0X2Zhdl9tb3ZpZXNfYnlfZ2VucmUgPC0gZnVuY3Rpb24odXNlcnMpIHsKICBmYXZfbW92aWVzX2J5X2dlbnJlIDwtIE1vdmllTGVuc2VFREFfSm9pbmVkICU+JSBmaWx0ZXIodXNlciAlaW4lIHVzZXJzLCByYXRpbmcgPT0gNSkgJT4lIGdyb3VwX2J5KHVzZXIpICU+JSBzdW1tYXJpc2UoYWNyb3NzKGModW5rbm93biwgQWN0aW9uLCBBZHZlbnR1cmUsIEFuaW1hdGlvbiwgYENoaWxkcmVuJ3NgLCBDb21lZHksIENyaW1lLCBEb2N1bWVudGFyeSwgRHJhbWEsIEZhbnRhc3ksIGBGaWxtLU5vaXJgLCBIb3Jyb3IsIE11c2ljYWwsIE15c3RlcnksIFJvbWFuY2UsIGBTY2ktRmlgLCBUaHJpbGxlciwgV2FyLCBXZXN0ZXJuKSxzdW0pKSAlPiUgbXV0YXRlKHVzZXIgPSBhcy5udW1lcmljKHVzZXIpKSAlPiUgIGFycmFuZ2UodXNlcikKICByZXR1cm4oZmF2X21vdmllc19ieV9nZW5yZSkKfQoKdG9wbm1vbml0b3JfZmF2X21vdmllcyA8LSBnZXRfZmF2X21vdmllc19ieV9nZW5yZSh0ZXN0VXNlcnMpCnRvcG5tb25pdG9yX2Zhdl9tb3ZpZXMKYGBgCkJlaSBkaWVzZW0gVGVzdCBkZXIgRnVua3Rpb24gd3VyZGUgd2llZGVyIGRhc3NlbGJlIFJlc3VsdGF0IGdlbmVyaWVydC4KCmBgYHtyfQojIGl0ZXJhdGUgdGhyb2d1aCBib3RoIHRlc3RfdXNlcnNfaQp0b3BubW9uaXRvcl9mYXZfbW92aWVzXzEgPC0gZ2V0X2Zhdl9tb3ZpZXNfYnlfZ2VucmUodGVzdFVzZXJzXzEpCnRvcG5tb25pdG9yX2Zhdl9tb3ZpZXNfMQoKdG9wbm1vbml0b3JfZmF2X21vdmllc18yIDwtIGdldF9mYXZfbW92aWVzX2J5X2dlbnJlKHRlc3RVc2Vyc18yKQp0b3BubW9uaXRvcl9mYXZfbW92aWVzXzIKCnRvcG5tb25pdG9yX2Zhdl9tb3ZpZXNfbGlzdCA8LSBsaXN0KHRvcG5tb25pdG9yX2Zhdl9tb3ZpZXNfMSwgdG9wbm1vbml0b3JfZmF2X21vdmllc18yKQpgYGAKRGllc2UgRnVua3Rpb24gaGF0IG51biBkaWUgVG9wIEdlbnJlcyBkZXIgdm9uIGRlbiBVc2VybiBiZXdlcnRldGVuIEZpbG1lbiBnZW5lcmllcnQuIEltIHp3ZWl0ZW4gRGF0YWZyYW1lIGlzdCBlcnNpY2h0bGljaCwgZGFzcyBzaWViZW4gdm9uIFVzZXIgNCAiZ2VsaWt0ZSIgRmlsbWUgdm9tIEdlbnJlIEFjdGlvbiBrb21tZW4uCgogCiMjIDEwLjQgVmVyZ2xlaWNoZSBwcm8gS3VuZGUgVG9wLUVtcGZlaGx1bmdlbiB2cyBUb3AtRmlsbWUgbmFjaCBHZW5yZXMsCmBgYHtyfQphIDwtIHRvcG5tb25pdG9yX3JlY29tWzEsMjoyMF0KYiA8LSB0b3BubW9uaXRvcl9mYXZfbW92aWVzWzEsMjoyMF0KCmJpbmRlZCA8LSByYmluZChhLCBiKQpiaW5kZWRfY29tcGxldGUgPC0gYmluZGVkICU+JSBhZGRfY29sdW1uKFR5cGUgPSBjKCJ0b3BubW9uaXRvcl9yZWNvbSIsICJ0b3BubW9uaXRvcl9mYXZfbW92aWVzIikpCmJpbmRlZF9jb21wbGV0ZQoKIyBwaXZvdF9sb25nZXIgYmluZGVkX2NvbXBsZXRlIGRhdGFmcmFtZQpiaW5kZWRfY29tcGxldGVfcGl2b3QgPC0gcGl2b3RfbG9uZ2VyKGJpbmRlZF9jb21wbGV0ZSwgY29scyA9IDE6MTksIG5hbWVzX3RvID0gImdlbnJlIiwgdmFsdWVzX3RvID0gInZhbHVlIikKYmluZGVkX2NvbXBsZXRlX3Bpdm90CgojIGNyZWF0ZSBjbGV2ZWxhbmQgcGxvdApnZ3Bsb3QoYmluZGVkX2NvbXBsZXRlX3Bpdm90LCBhZXMoeSA9IGdlbnJlLCB4ID0gdmFsdWUpKSArCiAgZ2VvbV9wb2ludChhZXMoY29sb3IgPSBUeXBlKSkgKyAKICBnZW9tX2xpbmUoYWVzKGdyb3VwPWdlbnJlKSkgKyAKICB0aGVtZV9taW5pbWFsKCkgKwogIGxhYnModGl0bGUgPSAiVG9wLU4gRW1wZmVobHVuZ2VuIHZzIFRvcC1GaWxtZSBuYWNoIEdlbnJlcyIsIHggPSAiVmFsdWUiLCB5ID0gIkdlbnJlIikgKwogIHRoZW1lKHBsb3QudGl0bGUgPSBlbGVtZW50X3RleHQoaGp1c3QgPSAwLjUpKQoKYGBgCkhpZXJiZWkgaGFuZGVsdCBlcyBzaWNoIHdpZWRlciB1bSBlaW5lbiBUZXN0IGbDvHIgZGVuIENsZXZlbGFuZCBQbG90LgoKCmBgYHtyfQpjcmVhdGVfY2xldmVsYW5kX3Bsb3QgPC0gZnVuY3Rpb24oYSwgYiwgZGF0YXRleHQsIG1vZGVsdGV4dCl7CiAgYmluZGVkIDwtIHJiaW5kKGEsIGIpCiAgYmluZGVkX2NvbXBsZXRlIDwtIGJpbmRlZCAlPiUgYWRkX2NvbHVtbihUeXBlID0gYygidG9wbm1vbml0b3JfcmVjb20iLCAidG9wbm1vbml0b3JfZmF2X21vdmllcyIpKQogIGJpbmRlZF9jb21wbGV0ZV9waXZvdCA8LSBwaXZvdF9sb25nZXIoYmluZGVkX2NvbXBsZXRlLCBjb2xzID0gMToxOSwgbmFtZXNfdG8gPSAiZ2VucmUiLCB2YWx1ZXNfdG8gPSAidmFsdWUiKQogIGdncGxvdChiaW5kZWRfY29tcGxldGVfcGl2b3QsIGFlcyh5ID0gZ2VucmUsIHggPSB2YWx1ZSkpICsKICAgIGdlb21fcG9pbnQoYWVzKGNvbG9yID0gVHlwZSkpICsgCiAgICBnZW9tX2xpbmUoYWVzKGdyb3VwPWdlbnJlKSkgKyAKICAgIHRoZW1lX21pbmltYWwoKSArCiAgICBsYWJzKHRpdGxlID0gcGFzdGUoIlRvcC1OIEVtcGZlaGx1bmdlbiB2cyBUb3AtRmlsbWUgbmFjaCBHZW5yZXMgbWl0IE1vZGVsbCIsIG1vZGVsdGV4dCwgImbDvHIiLCBkYXRhdGV4dCkgLCB4ID0gIlZhbHVlIiwgeSA9ICJHZW5yZSIpICsKICAgIHRoZW1lKHBsb3QudGl0bGUgPSBlbGVtZW50X3RleHQoaGp1c3QgPSAwLjUpKQp9CmBgYAoKRGllc2UgRnVua3Rpb24gZ2VuZXJpZXJ0IGRpZSBDbGV2ZWxhbmQgUGxvdHMuCgpgYGB7cn0KI21vZGVscyB1bmQgZGF0ZW5zYXR6IGZpbmRlbiB1bmQgenVtIHRpdGVsIGhpbnpmdWVnZW4uCm5fbW9kZWxzIDwtIGxpc3QoIklCQ0YiLCAiVUJDRiIsICJTVkRfMyIsICJTVkRfNSIpCm5fZGF0YXNldCA8LSBsaXN0KCJEYXRlbnNhdHogMSIsICJEYXRlbnNhdHogMiIpCgpjbnQgPC0gMQpmb3IgKGJfaSBpbiAxOmxlbmd0aChuX2RhdGFzZXQpKSB7CiAgZGF0YXRleHRfaSA8LSBuX2RhdGFzZXRbYl9pXQogIGZvciAoYV9pIGluIDE6bGVuZ3RoKG5fbW9kZWxzKSkgewogICAgbW9kZWx0ZXh0X2kgPC0gbl9tb2RlbHNbYV9pXQogICAgYl9kZiA8LSB0b3BubW9uaXRvcl9mYXZfbW92aWVzX2xpc3RbW2JfaV1dWzEsMjoyMF0KICAgIGFfZGYgPC0gdG9wX25fbGlzdFtbY250XV1bMSwyOjIwXQogICAgY250IDwtIGNudCArIDEKICAgIHByaW50KGNyZWF0ZV9jbGV2ZWxhbmRfcGxvdChhX2RmLCBiX2RmLCBkYXRhdGV4dF9pLCBtb2RlbHRleHRfaSkpCiAgfQp9CmBgYApFcyB3dXJkZW4gbnVuIDggQ2xldmVsYW5kIFBsb3RzIGdlbmVyaWVydC4gVmllciBmw7xyIGRlbiBlcnN0ZW4gcmVkdXppZXJ0ZW4gRGF0ZW5zYXR6IHVuZCB2aWVyIGbDvHIgZGVuIHp3ZWl0ZW4gcmVkdXppZXJ0ZW4gRGF0ZW5zYXR6LiBEaWUgdmllciBQbG90cyBwcm8gRGF0ZW5zYXR6IHN0ZWhlbiBmw7xyIGplIGVpbmVzIGRlciB1bnRlcnNjaGllZGxpY2hlbiBNb2RlbGxlLiBEYXJnZXN0ZWxsdCB3aXJkIGpld2VpbHMgZGFzIFJlc3VsdGF0IGRlcyBlcnN0ZW4gVXNlcnMgcHJvIERhdGVuc2F0ei4gSW0gZXJzdGVuIFBsb3QgKElCQ0YgZsO8ciBEYXRlbnNhdHogMSkgaXN0IGVyc2ljaHRsaWNoLCBkYXNzIGRlbSBVc2VyIDEwbWFsIERyYW1hIEZpbG1lIGVtcGZvaGxlbiBoYXQsIHfDpGhyZW5kIGVyIHNlbGJlciBzaWUgbnVyIDNtYWwgZ2VsaWt0IGhhdC4gRGVyIHNjaHdhcnplIEJhbGtlbiBlbnRzcHJpY2h0IGRlciBEaWZmZXJlbnouCgpEZXIgdmlzdWVsbGUgVmVyZ2xlaWNoIHp3aXNjaGVuIGRlbiBiZWlkZW4gRGF0ZW5zw6R0emVuIGzDpHNzdCBkZW4gU2NobHVzcyB6dSwgZGFzcyBiZWkgYmVpZGVuIGdyb3NzZSBEaXNrcmVwYW56ZW4gendpc2NoZW4gZGVuIEVtcGZlaGx1bmdlbiB1bmQgZGVuIHRhdHPDpGNobGljaGVuIExpa2VzIGJlc3RlaGVuLgoKIyMgMTAuNSBEZWZpbmllcmUgZWluZSBRdWFsaXTDpHRzbWV0cmlrIGbDvHIgVG9wLU4gTGlzdGVuIHVuZCB0ZXN0ZSBzaWUuCgpgYGB7cn0KIyB0ZXN0aW5nCnJvd1N1bXModG9wbm1vbml0b3JfcmVjb21bMjoyMF0gKiB0b3BubW9uaXRvcl9mYXZfbW92aWVzWzI6MjBdKSAvCihzcXJ0KHJvd1N1bXModG9wbm1vbml0b3JfcmVjb21bMjoyMF1eMikpKnNxcnQocm93U3Vtcyh0b3BubW9uaXRvcl9mYXZfbW92aWVzWzI6MjBdXjIpKSkKYGBgCgpIaWVyIGhhbmRlbHQgZXMgc2ljaCB1bSB3aWVkZXJ1bSB1bSBlaW5lbiBUZXN0LgoKYGBge3J9CmNvc2luZV8xMF8yZGYgPC0gZnVuY3Rpb24oZGYxLCBkZjIpIHsKICBjb3NpbmUgPC0gcm93U3VtcyhkZjFbMjoyMF0gKiBkZjJbMjoyMF0pIC8KICAoc3FydChyb3dTdW1zKGRmMVsyOjIwXV4yKSkqc3FydChyb3dTdW1zKGRmMlsyOjIwXV4yKSkpCiAgcmV0dXJuKGNvc2luZSkKfSAKCmNudCA8LSAxCmZvciAoYl9pIGluIDE6bGVuZ3RoKG5fZGF0YXNldCkpIHsKICBkYXRhdGV4dF9pIDwtIG5fZGF0YXNldFtiX2ldCiAgZm9yIChhX2kgaW4gMTpsZW5ndGgobl9tb2RlbHMpKSB7CiAgICBtb2RlbHRleHRfaSA8LSBuX21vZGVsc1thX2ldCiAgICBiX2RmIDwtIHRvcG5tb25pdG9yX2Zhdl9tb3ZpZXNfbGlzdFtbYl9pXV0KICAgIGFfZGYgPC0gdG9wX25fbGlzdFtbY250XV0KICAgIGNudCA8LSBjbnQgKyAxCiAgICBwcmludChwYXN0ZSgiTW9kZWw6IiwgbW9kZWx0ZXh0X2ksICJmdWVyOiIsIGRhdGF0ZXh0X2kpKQogICAgcHJpbnQoY29zaW5lXzEwXzJkZihhX2RmLCBiX2RmKSkKICB9Cn0KCmBgYAoKV2lyIGhhYmVuIHVucyBmw7xyIGRpZSBRdWFsaXTDpHRzbWV0cmlrIENvc2luZSBTaW1pbGFyaXR5IGVudHNjaGllZGVuLiBEYSB3aXIgcHJvIFVzZXIgendlaSBWZWt0b3JlbiBoYWJlbiwgaXN0IGRpZXMgZWluZSBnZWVpZ25ldGUgTWV0cmlrLCB1bSBkaWUgVG9wLU4gTGlzdGUgdW5kIGRpZSBUb3AtRmlsbWUgZGVzIEt1bmRlbiB6dSB2ZXJnbGVpY2hlbi4KRsO8ciBkYXMgTW9kZWwgSUJDRiB1bmQgZGVuIERhdGVuc2F0eiAxIHd1cmRlIGbDvHIgZGVuIHp3ZWl0ZW4gVXNlciBlaW5lIENvc2luZSBTaW1pbGFyaXR5IHZvbiAwLjg1IGJlcmVjaG5ldC4gRGllIMOcYmVyZWluc3RpbW11bmcgendpc2NoZW4gZGVuIFRvcC1OIGVtcGZvaGxlbmVuIEdlbnJlcyB1bmQgZGVuIFRvcC1GaWxtLUdlbnJlcyBkZXMgS3VuZGVuIGlzdCBhbHNvIHJlbGF0aXYgaG9jaC4gSW0gR2VnZW5zYXR6IGRhenUgaXN0IGJlaW0gc2VjaHN0ZW4gVXNlciBkaWUgU2ltaWxhcml0eSBsZWRpZ2xpY2ggMC40NyB1bmQgZGllIMOcYmVyZWluc3RpbW11bmcgZGFtaXQgbWVya2JhciB0aWVmZXIuIApJbm5lcmhhbGIgZGVzIGVyc3RlbiBEYXRlbnNhdHplcyBzaW5kIGRpZSBTaW1pbGFyaXRpZXMgcmVsYXRpdiBob2NoLiBCZWkgYWxsZW4gTW9kZWxsZW4gbGllZ2VuIGRpZSBXZXJ0ZSBncsO2c3N0ZW50ZWlscyDDvGJlciAwLjcuIERpZXMgZ2lsdCBhdWNoIGbDvHIgZGVuIHp3ZWl0ZW4gRGF0ZW5zYXR6LiBOYWNoIG9iZW4gZ2VoZW4gZGllIFdlcnRlIGJpcyBmYXN0IDEgbmFjaCB1bnRlbiBiaXMgMC40LgpWZXJnbGljaGVuIHp3aXNjaGVuIGRlbSBlcnN0ZW4gdW5kIHp3ZWl0ZW4gRGF0ZW5zYXR6LCBzaW5kIGRpZSBXZXJ0ZSBlYmVuZmFsbHMgw6RobmxpY2guIFdpciBoYWJlbiB6d2FyIGtlaW5lIEF1c3dlcnR1bmcgYmVyZWNobmV0LCBkb2NoIHNpbmQgZGllIFphaGxlbiBpbSBnbGVpY2hlbiwgZWhlciBob2hlbiwgQmVyZWljaC4K